The case against the generic use of PATCH and PUT in REST APIs
There is a lot of confusion among developers on how to implement updates using a REST API. The tools we are given (PATCH
and PUT
) don't actually map cleanly to how we would implement an update in our business code. Let's start by considering what those HTTP verbs mean:
PATCH
: perform a partial update to a resource, particularly individual properties of that resource.
PUT
: overwrite/replace an entire resource with the supplied representation.
At a basic level, PATCH
and PUT
seem like all we need to perform updates on a resource. However, experience demonstrates that models and business processes really don't work that way -- meaning applications typically have very specific ways in which mutations occur.
For example, how often do you see code like this?:
app.patch('/persons/:personId', async (req, res) => {
const person = await getPerson(req.params.personId);
Object.assign(person, req.payload);
return res.json(await person.save());
});
// OR
app.put('/persons/:personId', async (req, res) => {
const person = await getPerson(req.params.personId);
await person.remove();
const replacement = new Person(
Object.assign(
{ id: req.params.personId },
req.payload
)
);
return res.json(await replacement.save());
});
I'll admit this is probably an overly simplistic example; however, I have seen many developers write code similar to this. In some cases, this may be acceptable -- some models are so simple they don't need behaviors outside of what CRUD operations provide. But realistically, your domain model is going to be rich in behavior and these generic updates will be useless.
Which is why I would argue that PATCH
and PUT
make little sense for exposing updates to a domain model. Instead, you should model updates as intent-based POST
s or property-specific PATCH
es that map to business actions against the root or nested entities.
To understand why I'm advocating these principals, we need to understand exactly why PATCH
and PUT
are problematic when used to update entities.
Problems with PATCH
and PUT
Imagine we have a shopping cart and we want to model an order (I will use Mongoose to represent the entities):
// Pretend I've configured Mongoose to use UUIDs
const { Schema } = require('mongoose');
const LineItem = new Schema({
product: String,
count: Number,
});
// I've never built a shopping cart, so forgive me
// if this violates the international conventions
// on shopping cart design.
const Statuses = {
Creating: 'creating',
Finalizing: 'finalizing',
Paid: 'paid',
Processing: 'processing',
Shipped: 'shipped',
Closed: 'closed',
Cancelled: 'cancelled',
};
const Order = new Schema({
customer: String,
discountCode: String,
description: String,
status: {
type: String,
default: Statuses.Creating,
enum: Object.values(Statuses),
},
items: [LineItem],
});
If we had an order:
{
id: '32b8b525-5cb9-4215-813c-d46a4b7be072',
customer: '2ae13653-1601-453b-bcad-10cd1b157e50',
discountCode: 'funtimes123',
description: 'Wedding Registry Purchase for Susan',
status: 'creating',
items: [
{
id: 'd9cac22e...',
product: '72489d1a...',
count: 2,
},
{
id: 'b5bc6f4a...',
product: 'e326efdf...',
count: 1,
},
],
}
Consider what the client would need to do to update this resource using PUT
or PATCH
.
PUT /orders/{orderId}
To perform a replacement, you would need to copy the representation of the model, make the appropriate changes, and resubmit it to the application. This causes a lot of complexity for both the API client and server:
- The application client has to know how to correctly construct the model.
- The server can either perform an overwrite of existing data or attempt to determine the difference between the incoming model and the current state.
How do we handle the consequences of this update? For instance, what if we don't have the inventory to fulfill the order? What if the discount code cannot be used in combination with specific items?
The problem with PUT
is that both the client and server need to be programmed to handle the myriad of error cases that could happen for the root or nested entities within a single API call.
There is also an issue with application state. When using replacement as a strategy for updates, you will need to consider how the changes (potentially a bunch of unrelated updates) affect the entity. If your entity is a state machine, you have the lifecycle of the root and nested aggregates to consider. You will also have to determine whether actions that are triggered by updates need to be "re-triggered", "ignored", or "fired for the first time".
I think it's obvious that PUT
would not be the correct verb to use. As an application developer, I wouldn't want the client to have to "assemble" requests from what it thinks is the current application state. More importantly, I wouldn't want the client to have to take on the responsibility of understanding the internal domain model (which it would need to if it wanted to ensure it was consistently successful in making updates).
Prakash Subramaniam discusses this idea in REST API Design - Resource Modeling:
PUT puts too much internal domain knowledge into the client. The client shouldn't be manipulating internal representation; it should be a source of user intent.
So if replacements are a typically a bad idea, what's wrong with partial updates?
PATCH /orders/{orderId}
Let me first start by clarifying what we mean by a partial update. In this case, I mean "generic updates" to the properties of the root entity or its aggregates. I don't mean isolated and controlled updates that map directly to business activities.
An example of a partial update using our previous example might look like this:
/* PATCH /orders/:orderId, with payload: */
{
// OMG Susan, I'm so sorry!
description: 'Funeral Purchase for Susan',
status: 'finalizing',
items: [
{
id: 'd9cac22e...',
product: '72489d1a...',
count: 3,
},
{
id: '8ec62a5f...',
product: 'f771a648...',
count: 1,
},
],
}
Once again, a contrived example, but I hope you get the point. In this example, there are three properties being updated, two of which I'd argue is inappropriate for PATCH
.
Informational Updates
Updating the Order.description
is probably the only appropriate use of PATCH
in this example. It's unlikely that changes to this property will spawn any sort of business action or workflow, especially if this property is not indexed (simply used to label the order in the UI). However, if we implement the pattern I'm going to recommend, we probably wouldn't want to expose updating the description as a general update to the resource because it would be inconsistent with our strategy.
Updating Properties of a State Machine
Hopefully, you were screaming at the screen when you saw the update to the Order.status
field. Clients should never decide the current state of an entity, instead, they should request transitions of that entity to the desired state.
Spaghetti Code Handlers
Even if this API call was considered a request to transition the entity's state, does it make a lot of sense to bundle changes to Order.description
and Order.items
? The answer is a resounding, NO!
Think about how you have to write your backend logic to handle this request. It probably looks something like this:
app.patch('/orders/:orderId', async (req, res) => {
const { description, status, items } = req.payload;
let order = await getOrder(req.params.orderId);
try {
if (description) {
order = await updateDescription(order, description);
}
if (status) {
// OMG - I hate myself just writing this!
order = await initiateStateTransition(order, status);
}
if (items) {
order = await updateItemsInOrder(order, items);
}
} catch (error) {
return mapErrorToResponse(error, res);
}
return res.send(order);
});
At its cleanest, the generic update handler becomes a logic router that has to determine the changes occurring to the model and delegate those actions to the correct business workflows. From my experience, it's more common to find a handler full of spaghetti code.
Handling Nested Entities
A less obvious problem is the update to Order.items
. In the example API call, we are performing a replacement of the property with the values supplied in the request. This strategy is effectively a PUT
targeting the nested entity collection /orders/:orderId/items
if you think about it. This means we run into the same problems we encountered with PUT
.
Of course, we could model this differently. Let's try a batch update strategy. In this strategy, we will supply the entity and an action to take. If an existing entity is not referenced in the update, we will assume no change:
/* PATCH /orders/:orderId, with payload: */
{
items: [
{
action: 'update',
id: 'd9cac22e...',
count: 3,
},
{
action: 'remove',
id: 'b5bc6f4a...',
},
{
action: 'add',
id: '8ec62a5f...',
product: 'f771a648...',
count: 1,
},
],
}
Perhaps this is a better way of modeling updates to a nested collection, and in some cases, you may need to support a batch update scenario. The weakness in this strategy is the added complexity of both the server and the client. For instance, what happens when an item cannot be added or modified because there is not enough inventory? Should the request be rejected in its entirety? Or should the server return a partial failure? More importantly, the server not only needs to be able to handle the batch update, but the client needs to know how to assemble the update and then respond to a much more complex error.
A Better Strategy For Updates
You probably should not be surprised by the recommendation I'm about to make. As we have seen, both replacement (PUT
) and generic partial update (PATCH
) strategies are rife with implementation problems. This leaves us with only one real alternative: specific updates sometimes referred to as intents.
An intent is best explained by Prakash:
The way to escape low-level CRUD is to create business operation or business process resources, or what we can call as "intent" resources that express a business/domain level "state of wanting something" or "state of the process towards the end result".
In layman terms, an intent represents a request you want to make of an entity. It's almost like a command, except having been boxed in by the available HTTP verbs, we are left with asking the server to create a change for us. From a RESTful standpoint, it's perhaps not the most elegant solution, but from a backend/domain model perspective, it's essential (see my thoughts on REST purity below).
Using the previous example, let's design a better API that accounts for the types of changes we would want to occur with the Order
entity:
Method | URI | Description |
---|---|---|
PATCH | /orders/:orderId/info | Update properties that do not affect the lifecycle of the entity |
PATCH | /orders/:orderId/discount | Update the discount code |
POST | /orders/:orderId/finalize | Finalize the order - maybe this is done when the customer is ready to review the shopping cart and pay. |
POST | /orders/:orderId/cancel | Cancel the order. |
As you can see, I'm not entirely against the use of PATCH
. However, you will see that the way I use the verb treats some properties of the root entity as if they were nested entities.
I use POST
to represent intents. In most cases, from my experience, an intent is used to interact with a state machine (mapping directly to a business action). Since we are conceptually not "updating the status" (finalizing or canceling an order can have all kinds of consequences), I think it makes sense to not use PATCH
.
Now that we've separated these actions into their own routes on the backend, implementing the functionality becomes extremely straightforward:
app.patch('/orders/:orderId/info', async (req, res) => {
const { description } = req.payload;
let order = await getOrder(req.params.orderId);
try {
order = await updateDescription(order, description);
} catch (error) {
return mapErrorToResponse(error, res);
}
return res.send(order);
});
app.post('/orders/:orderId/cancel', async (req, res) => {
let order = await getOrder(req.params.orderId);
try {
order = await cancelOrder(order);
} catch (error) {
return mapErrorToResponse(error, res);
}
return res.send(order);
});
// ...I'll leave the rest to your imagination.
Since LineItems
represent a nested entity collection, they should be treated as a nested resources. This removes the need to develop complex (and sometimes incomprehensible) batch APIs or replacement updates:
Method | URI | Description |
---|---|---|
POST | /orders/:orderId/items | Add an item to the order. |
PATCH | /orders/:orderId/items/:itemId/count | Update the quantity of that item. |
DELETE | /orders/:orderId/items/:itemId | Add an item to the order. |
The Orders
API is now significantly less ambiguous. REST API endpoints now map 1:1 with business actions in the domain model. If you publish events (and you should) then business actions will also map 1:1 with an event:
POST /orders/:orderId/cancel
-> cancelOrder(order)
-> publish('orders.status.cancelled', order)
Conclusion
I hope you have seen the value in this strategy. I discovered the need to separate API calls into specific updates after having implemented routes in all of the bad ways mentioned in this post. Our team had generic update routers, batch endpoints for nested entities, an endpoint that allowed another system to change the status of an entity managed by a state machine (OMG this was a nightmare, but not of my doing), etc. Needless to say, these were hard-won lessons and I hope you will discover this article (or Prakash's) before you make the same mistakes.
Thoughts on REST Purity
I have no doubt that this post will irk half the internet, particularly the REST purists. My preemptive response is that PATCH
and PUT
may make more sense when dealing with mediums other than business models. For instance, it makes total sense that you would use PUT
to update an audio file -- how do you partially change the bits of a binary format in a way that makes sense?
Likewise, PATCH
can be useful for simple scenarios. For instance, perhaps we can update the metadata of the previously mentioned audio file. Using PATCH
to change basic information like author, title, album, whatever makes sense.
However, if we cast our net wider and consider other (modern) forms of API communication, we will find that this notion of purpose-driven updates is actually the norm. For instance, GraphQL
, served over HTTP, condenses operations into two categories queries and mutations. Falcor does something similar with its RPC protocol. Other protocols like gRPC don't even differentiate between query and mutation request -- there's simply targeted requests.
The benefit of RESTful APIs is their ubiquity. We obviously want to design APIs that are immediately understood, allowing integration to occur as rapidly as possible. I would contend that using intent-driven updates does not necessarily violate our shared understanding of REST, though I can understand why people might think it does. Can we really consider an intent a resource when they are typically only created and never read, updated, or deleted? The pragmatist in me thinks this is a debate better left to the clergy of the church of REST. In the meantime, the rest of us have services to deliver and code to maintain. I'm more than willing to revisit this conversation when REST/HTTP provides a better methodology for updates.
Resources
Fantastic Blog Post by Prakash Subramaniam which validated many of the thoughts I had on RESTful updates: https://www.thoughtworks.com/insights/blog/rest-api-design-resource-modeling
Stumbling my way through the great wastelands of enterprise software development.