Skip to content

7.1.3 Updating data on the server

Before you write any controller code for handling HTTP PUT or PATCH commands, you should take a moment to consider the elephant in the room: why are there two different HTTP methods for updating resources?

Although it’s true that PUT is often used to update resource data, it’s actually the semantic opposite of GET. Whereas GET requests are for transferring data from the server to the client, PUT requests are for sending data from the client to the server.

In that sense, PUT is really intended to perform a wholesale replacement operation rather than an update operation. In contrast, the purpose of HTTP PATCH is to perform a patch or partial update of resource data.

For example, suppose you want to be able to change the address on an order. One way we could achieve this through the REST API is with a PUT request handled like this:

java
@PutMapping(path="/{orderId}", consumes="application/json")
public TacoOrder putOrder(
          @PathVariable("orderId") Long orderId,
          @RequestBody TacoOrder order) {
  order.setId(orderId);
  return repo.save(order);
}

This could work, but it would require that the client submit the complete order data in the PUT request. Semantically, PUT means “put this data at this URL,” essentially replacing any data that’s already there. If any of the order’s properties are omitted, that property’s value would be overwritten with null. Even the tacos in the order would need to be set along with the order data or else they’d be removed from the order.

If PUT does a wholesale replacement of the resource data, then how should you handle requests to do just a partial update? That’s what HTTP PATCH requests and Spring’s @PatchMapping are good for. Here’s how you might write a controller method to handle a PATCH request for an order:

java
@PatchMapping(path="/{orderId}", consumes="application/json")
public TacoOrder patchOrder(@PathVariable("orderId") Long orderId,
          @RequestBody TacoOrder patch) {
              
  TacoOrder order = repo.findById(orderId).get();
  if (patch.getDeliveryName() != null) {
  order.setDeliveryName(patch.getDeliveryName());
  }
  if (patch.getDeliveryStreet() != null) {
  order.setDeliveryStreet(patch.getDeliveryStreet());
  }
  if (patch.getDeliveryCity() != null) {
  order.setDeliveryCity(patch.getDeliveryCity());
  }
  if (patch.getDeliveryState() != null) {
  order.setDeliveryState(patch.getDeliveryState());
  }
  if (patch.getDeliveryZip() != null) {
  order.setDeliveryZip(patch.getDeliveryState());
  }
  if (patch.getCcNumber() != null) {
  order.setCcNumber(patch.getCcNumber());
  }
  if (patch.getCcExpiration() != null) {
  order.setCcExpiration(patch.getCcExpiration());
  }
  if (patch.getCcCVV() != null) {
  order.setCcCVV(patch.getCcCVV());
  }
  return repo.save(order);
}

The first thing to note here is that the patchOrder() method is annotated with @PatchMapping instead of @PutMapping, indicating that it should handle HTTP PATCH requests instead of PUT requests.

But the one thing you’ve no doubt noticed is that the patchOrder() method is a bit more involved than the putOrder() method. That’s because Spring MVC’s mapping annotations, including @PatchMapping and @PutMapping, specify only what kinds of requests a method should handle. These annotations don’t dictate how the request will be handled. Even though PATCH semantically implies a partial update, it’s up to you to write code in the handler method that actually performs such an update.

In the case of the putOrder() method, you accepted the complete data for an order and saved it, adhering to the semantics of HTTP PUT. But in order for patchMapping() to adhere to the semantics of HTTP PATCH, the body of the method requires more intelligence. Instead of completely replacing the order with the new data sent in, it inspects each field of the incoming TacoOrder object and applies any non-null values to the existing order. This approach allows the client to send only the properties that should be changed and enables the server to retain existing data for any properties not specified by the client.

There’s more than one way to PATCH

The patching approach applied in the patchOrder() method has the following limitations:

  • If null values are meant to specify no change, how can the client indicate that a field should be set to null?
  • There’s no way of removing or adding a subset of items from a collection. If the client wants to add or remove an entry from a collection, it must send the complete altered collection.

There’s really no hard-and-fast rule about how PATCH requests should be handled or what the incoming data should look like. Rather than sending the actual domain data, a client could send a patch-specific description of the changes to be applied. Of course, the request handler would have to be written to handle patch instructions instead of the domain data.

In both @PutMapping and @PatchMapping, notice that the request path references the resource that’s to be changed. This is the same way paths are handled by @GetMappingannotated methods.

You’ve now seen how to fetch and post resources with @GetMapping and @PostMapping. And you’ve seen two different ways of updating a resource with @PutMapping and @PatchMapping. All that’s left is handling requests to delete a resource.

Released under the MIT License.