π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!
βUpdating a resource with PUT is like replacing your entire banana with a new one. PATCH is like carefully changing just a single brown spot on the banana to yellow. ππβ
In the jungle of REST API design, you’ll eventually need to update a resource. You reach into your toolbox and pull out two tools: HTTP PUT and HTTP PATCH. They look similar, and many developers use them interchangeably. But this is a classic rookie mistake! Using the wrong tool can lead to inefficient, non-standard, and sometimes even buggy APIs. One is a sledgehammer, the other a scalpel, and knowing which one to grab is the mark of a professional.
Problem Statement: Are They Just the Same?
A junior monkey is told to implement an “update user profile” feature. They create a single endpoint, maybe /api/profiles/monkey-dev
, and let the frontend send a JSON object to it. They might use PUT or they might use PATCH, thinking it doesn’t matter. But what happens when the user only wants to update their bio? Does the frontend have to send the user’s address and phone number too? What if it sends "phoneNumber": null
? Does that mean “don’t update the phone number” or “set the phone number to null”? This ambiguity is where APIs become fragile.
The “Right” Way: Understanding the HTTP Contract
The HTTP specification gives us clear, distinct definitions for PUT and PATCH. Understanding this contract is the first step to building clean, predictable APIs.
Aspect | PUT | PATCH |
---|---|---|
Meaning | Full Replacement | Partial Update |
Payload | The client sends the complete new representation of the resource. All fields are required. | The client sends only the fields that need to change. |
Effect on Missing Fields | If a client sends a PUT request, any fields not included in the request body should be set to their default value (often null ). | Fields not included in the request body are not changed. |
Idempotency | Yes. Sending the same PUT request multiple times will always result in the same final state for the resource. | Not necessarily. A PATCH request like “add 10 to the score” is not idempotent if called twice. |
Analogy | Replacing a file on a server. You upload the whole new file. | A set of instructions to edit a file, like “change line 5 to ‘hello’”. |
1οΈβ£ Implementing an Idempotent PUT Endpoint
When you provide a PUT endpoint, you’re telling the client, “Give me the *entire* user profile, exactly as it should be from now on.”
Let’s assume we have a UserProfile
entity.
// UserProfile entity (simplified)
@Entity
public class UserProfile {
@Id
private String username;
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
// UserProfileDto for the PUT request
public class UserProfileDto {
private String address;
private String phoneNumber;
private String bio;
// Getters and setters...
}
Your controller and service would look like this:
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
private final UserProfileService userProfileService;
// ... constructor ...
@PutMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile updateUserProfile(@PathVariable String username, @RequestBody UserProfileDto profileDto) {
return userProfileService.fullyUpdateUserProfile(username, profileDto);
}
}
@Service
public class UserProfileService {
private final UserProfileRepository userProfileRepository;
// ... constructor ...
public UserProfile fullyUpdateUserProfile(String username, UserProfileDto profileDto) {
// Find the existing profile or throw an exception
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Replace EVERY field with the data from the DTO
existingProfile.setAddress(profileDto.getAddress());
existingProfile.setPhoneNumber(profileDto.getPhoneNumber());
existingProfile.setBio(profileDto.getBio());
return userProfileRepository.save(existingProfile);
}
}
The key takeaway: The service logic for PUT is simple. You replace everything. If the client sends a DTO where `phoneNumber` is `null`, the `phoneNumber` in the database will be set to `null`. This is the contract of replacement.
2οΈβ£ Implementing a PATCH Endpoint (and Its Complexities)
PATCH is more efficient for the client. They only send what changed. But this makes the backend developer’s job harder. How do we model a DTO where `null` means “set to null” and “not present” means “do not change”?
Strategy: DTO with `java.util.Optional`
A clean, type-safe way to handle this in Java is to wrap every field in the DTO with an `Optional`. This makes the intent explicit.
// PatchUserProfileDto for the PATCH request
public class PatchUserProfileDto {
private Optional<String> address = Optional.empty();
private Optional<String> phoneNumber = Optional.empty();
private Optional<String> bio = Optional.empty();
// Getters and setters for the Optionals...
}
Now, the service logic can check if a field was present in the JSON payload.
@RestController
@RequestMapping("/api/profiles")
public class UserProfileController {
// ... PUT method from before ...
@PatchMapping("/{username}")
@ResponseStatus(HttpStatus.OK)
public UserProfile patchUserProfile(@PathVariable String username, @RequestBody PatchUserProfileDto patchDto) {
return userProfileService.partiallyUpdateUserProfile(username, patchDto);
}
}
@Service
public class UserProfileService {
// ... PUT service method from before ...
public UserProfile partiallyUpdateUserProfile(String username, PatchUserProfileDto patchDto) {
UserProfile existingProfile = userProfileRepository.findById(username)
.orElseThrow(() -> new ResourceNotFoundException("Profile not found"));
// Use ifPresent to update only the fields that were provided in the request
patchDto.getAddress().ifPresent(existingProfile::setAddress);
patchDto.getPhoneNumber().ifPresent(existingProfile::setPhoneNumber);
patchDto.getBio().ifPresent(existingProfile::setBio);
return userProfileRepository.save(existingProfile);
}
}
With this pattern, the intent is perfectly clear:
- If the client sends
{"bio": "A new bio"}
, only the bio is updated. - If the client sends
{"address": null}
, the address is explicitly set to null. - If the client sends
{}
, nothing is updated, and the original resource is returned.
π‘ Monkey-Proof Tips
- Network Efficiency vs. Backend Complexity: PATCH is more network-efficient because it sends smaller payloads. This is great for mobile clients or low-bandwidth environments. However, as we’ve seen, this efficiency comes at the cost of more complex logic on the backend to handle partial updates. PUT is less efficient on the wire but often simpler to implement.
- Consider JSON Merge Patch (RFC 7396): For more advanced scenarios, look into the JSON Merge Patch standard. It formalizes the rules for patching. In a Merge Patch, if a client sends
"field": null
, it means the field should be removed/nulled. If a field is absent from the patch document, it remains untouched. Libraries exist to apply these patches automatically, which can simplify your service logic. - Use the Right Tool for the Job: Don’t just default to one or the other. If your use case is a “settings” page where a user updates their entire profile at once, PUT is a perfect fit. If you have a feature where a user can quickly update just their status message, PATCH is the superior choice.
π Challenge
It’s time to prove you know the difference between the sledgehammer and the scalpel.
- Implement a `UserProfile` Entity and Repository: Create a simple `UserProfile` entity with fields like `username` (ID), `address`, `phoneNumber`, and `bio`.
- Build the PUT Endpoint: Create a `PUT /api/profiles/{username}` endpoint. Use a standard `UserProfileDto` that contains all fields. Write a test to prove that sending a DTO with a `null` value for `phoneNumber` indeed nullifies it in the database.
- Build the PATCH Endpoint: Create a `PATCH /api/profiles/{username}` endpoint. Use the `PatchUserProfileDto` strategy with `Optional` wrappers for each field. Write tests to prove:
- Sending only a `bio` updates only the `bio`.
- Sending an `address` of `null` updates the address to `null`.
- Sending an empty JSON object
{}
doesn’t change the entity at all.
π You’ve now mastered one of the most commonly misunderstood parts of REST API design. By respecting the contract of PUT and PATCH, you’re building APIs that are more predictable, efficient, and professional. Go forth and update resources with confidence!