While very few resources are considered immutable, there are often certain attributes of a resource that we can safely assume won’t change out from under us. In particular, a resource’s unique identifier is one of these attributes. But what if we want to rename a resource? How can we do so safely? Further, what if we want to move a resource from belonging to one parent resource to another? Or duplicate a resource? We’ll explore a safe and stable method for these operations, covering both copying (duplication) and moving (changing a unique identifier or changing a parent) of resources in an API.
In an ideal world, our hierarchical relationships between resources are perfectly designed and forever immutable. More importantly, in this magical world, users of an API never make mistakes or create resources in the wrong location. And they certainly never realize far too late that they’ve made a mistake. In this world, there should never be a need to rename or relocate a resource in an API because we, as API designers, and our customers, as API consumers, never make any mistakes in our resource layout and hierarchy.
This is the world we explored in chapter 6 and discussed in detail in section 6.3.6. Unfortunately, this world is not the one we currently exist in, and therefore we have to consider the possibility that there will come a time where a user of an API needs the ability to move a resource to another parent in the hierarchy or change the ID of a resource.
To make things more complicated, there may be scenarios where users need to duplicate resources, potentially to other locations in the hierarchy. And while both of these scenarios seem quite straightforward at a quick glance, like most topics in API design they lead us down a rabbit hole full of questions that need to be answered. The goal of this pattern is to ensure that API consumers can rename and copy resources throughout the resource hierarchy in a safe, stable, and (mostly) simple manner.
Since we cannot use the standard update method in order to move or copy a resource, we’re left with the obvious next best choice: custom methods. Luckily, the high-level idea of how these copy-and-move custom methods might work is straightforward. As with most API design issues, the devil is in the details, such as in copying a ChatRoom
resource as well as moving Message
resources between different ChatRoom
parent resources.
abstract class ChatRoomApi { @post("/{id=chatRooms/*/messages/*}:move") ❶ MoveMessage(req: MoveMessageRequest): Message; ❷ @post("/{id=chatRooms/*}:copy") ❶ CopyChatRoom(req: CopyChatRoomRequest): ChatRoom; ❷ }
❶ For both custom methods, we use the POST HTTP verb and target the specific resource we want to copy or move.
❷ For both custom methods, the response is always the newly moved or copied resource.
Not shown here are quite a few important and subtle questions. First, when you copy or move a resource, do you choose a unique identifier or does the service do so as though the new resource is being created? Is there a difference when working across parents (where you might want the same identifier as before, just belonging to a different parent) versus within the same parent (where you, presumably, just want the ability to change the identifier)?
Next, when you copy a ChatRoom
resource, do all of the Message
resources belonging to that ChatRoom
get copied as well? What if there are large attachments? Does that extra data get copied? And the questions don’t end there. We still need to figure out how to ensure that resources being moved (or copied) are not currently being interacted with by other users, what to do about the old identifiers, as well as any inherited metadata such as access control policies.
In short, while this pattern relies on nothing more than a specific custom method, we’re quite far from simply defining the method and calling it done. In the next section, we’ll dig into all of these questions in order to land on an API surface that ensures safe and stable resource copying (and moving).
As we saw in section 17.2, we can rely on custom methods to both copy and move resources around the hierarchy of an API. What we haven’t looked at are some of the important nuances about these custom methods and how they should actually work. Let’s start with the obvious: how should we determine the identifier of the newly moved or copied resource?
As we learned in chapter 6, it’s generally best to allow the API service itself to choose a unique identifier for a resource. This means that when we create new resources we might specify the parent resource identifier, but the newly created resource would end up with an identifier that is completely out of our control. And this is for a good reason: when we choose how to identify our own resources, we tend to do so pretty poorly. The same scenario shows up when it comes to copying and moving resources. In a sense, both of these are sort of like creating a new resource that looks exactly like an existing one and then (in the case of a move) deleting the original resource.
But even if the newly created resource has an identifier chosen by the API service, what should that be? It turns out that the most convenient option depends on our intent. If we’re trying to rename a resource such that it has a new identifier but exists in the same position in the resource hierarchy, then it might be quite important for us to choose the new identifier. This is especially so if the API permits user-specified identifiers, since we’re likely to be renaming a resource from some meaningful name to another meaningful name (for example, something like databases/database-prod
to databases/database-prod-old
). On the other hand, if we’re trying to move something from one position in the hierarchy to another, then it might actually be a better scenario if the new resource has the same identifier but belongs to a new parent (e.g., moving from an existing identifier of chatRooms/1234/messages/abcd
to chatRooms/5678/messages/abcd
, note the commonality of messages/abcd
). Since the scenarios differ quite a bit, let’s look at each one individually.
Choosing an identifier for a duplicate resource turns out to be pretty straightforward. Whether or not you’re copying the resource into the same or a different parent, the copy method should act identically to the standard create method. This means that if your API has opted for user-specified identifiers, the copy method should also permit the user to specify the destination identifier for the newly created resource. If it supports only service-generated identifiers, it should not make an exception and permit user-specified identifiers just for resource duplication. (If it did, this would be a loophole through which anyone could choose their own identifiers by simply creating the resource and then copying the resource to the actual intended destination.)
The result of this is that the request to copy a resource will accept both a destination parent (if the resource type has a parent), and, if user-specified identifiers are permitted, a destination ID as well.
interface CopyChatRoomRequest { id: string; ❶ destinationId: string; ❷ } interface CopyMessageRequest { id: string; ❶ destinationParent: string; ❸ destinationId: string; ❷ }
❶ We always need to know the ID of the resource to be copied.
❷ This field is optional. It should only be present if the API supports user-specified identifiers.
❸ When the resource has a parent in the hierarchy, we should specify where the newly copied resource should end up.
This can lead to a few surprising results. For example, if the API doesn’t support user-chosen identifiers, the request to copy any top-level resources takes only a single parameter: the ID of the resource to be copied. In another case, it’s possible that we might want to copy a resource into the same parent. In this case, the destinationParent
field would be identical to the parent of the resource pointed to in the id
field.
Additionally, even in cases where the API supports user-chosen identifiers, we might want to rely on an auto-generated ID when duplicating a resource. To do this, we would simply leave the destinationId
field blank and allow that to be the way we express to the service, “I’d like you to choose the destination identifier for me.”
Finally, when user-specified identifiers are supported, it’s possible that the destination ID inside the destination parent might already be taken. In this case, it might be tempting to revert to a server-generated identifier in order to ensure that the resource is successfully duplicated. This should be avoided, however, as it breaks the user’s assumptions about the destination of the new resource. Instead, the service should return the equivalent of a 409 Conflict
HTTP error and abort the duplication operation.
When it comes to moving resources, we actually have two subtly different scenarios. On the one hand, users may be most concerned about relocating a resource to another parent in the resource hierarchy, such as moving a Message
resource from one ChatRoom
to another. On the other hand, users may want to rename a resource, where the resource is technically moved to the same parent with a different destination ID. It’s also possible that we might want to do both at the same time (relocate and rename). To further complicate things, we must remember that renaming resources is only something that makes sense if the API supports user-chosen identifiers.
To handle these scenarios, it might be tempting to use two separate fields again, as in listing 17.2, where you have a destination parent as well as a destination identifier. In this case though, we always know the final identifier no matter what (compared to the copy method where we might rely on a server-generated identifier). The ID is either the same as the original resource except for a different parent or a completely new one of our choosing. As a result, we can use a single destinationId
field and enforce some constraints on the value for that field depending on whether user-chosen identifiers are supported. If they are, then any complete identifier is acceptable. If not, then the only valid new identifiers are those that exclusively change the parent portion of the ID and keep the rest the same.
All of this leads us to a move request structure that accepts only two fields: the identifier of the resource being moved and the intended destination of the resource after the relocation is complete.
interface MoveChatRoomRequest { ❶ id: string; ❷ destinationId: string; } interface MoveMessageRequest { id: string; ❷ destinationId: string; ❸ }
❶ Since ChatRoom resources have no hierarchical parents, this method only makes sense if user-specified identifiers are supported by the API.
❷ We always need to know the ID of the resource to be copied.
❸ Since Message resources have parents, we can move the resource to different parents by changing the destination ID.
As you can see, for top-level resources (those with no parents) the entire move method only makes sense if the API supports user-specified identifiers. This stands in stark contrast to the copy method, which still makes sense in this case. The issue is primarily that moving accomplishes two goals: relocating a resource to a different parent and renaming a resource. The latter only makes sense given support for user-specified identifiers. The former only makes sense if the resource has a parent. In cases where neither of these apply, the move method becomes entirely irrelevant and should not be implemented at all.
Now that we have an idea of the shape and structure of requests to copy and move resources around, let’s keep moving forward and look at how we handle even more complicated scenarios.
So far, we’ve worked with the condition that each resource we intend to move or copy is fully self-contained. In essence, our big assumption was that the resource itself has no additional information that needs to be copied outside of a single record. This assumption certainly made our work a bit easier, but unfortunately it’s not necessarily a safe assumption. Instead, we have to assume that there will be additional data associated with resources that need to be moved or copied as well. How do we handle this? Should we even bother with this extra data?
The short answer is yes. Put simply, the external data and associated resources (e.g., child resources) are rarely there without purpose. As a result, copying or moving a resource without including this associated data is likely to lead to a surprising result: a new destination resource that doesn’t behave the same as the source resource. And since one of the key goals of a good API is predictability, it becomes exceptionally important to ensure that the newly copied or moved resource is identical to the source resource in as many ways as possible. How do we make this happen?
First, whether copied or moved, all child resources must be included. For example, this means that if we copied or moved a ChatRoom
resource, all the Message
child resources must also be copied or moved with the new parent ID. Obviously this is far more work than updating a single row. Further, the number of updates is dependent on the number of child resources, meaning that, as you’d expect, copying a resource with few child resources will take far less time than a similar resource with a large number of child resources. Table 17.1 shows an example of the identifiers of resources before and after a move or copy operation.
While our issues with copying are complete here, this is unfortunately not the case when resources have been moved. Instead, after we’ve finished updating all of the IDs, we’ve actually created an entirely new problem: anywhere else that previously pointed at these resources now has a dead link, despite the resource still existing! In the next section, we’ll explore how to handle these references, both internal and external.
While we’ve decided that all child resources must be copied or moved along with the target resource, we’ve said nothing so far about related resources. For example, let’s imagine that our chat API supports reporting or flagging messages that might be inappropriate with a MessageReviewReport
resource. This resource is sort of like a support case of a message that has been flagged as inappropriate and should be reviewed by the support team. This resource is not necessarily a child of a Message
resource (or even a ChatRoom
resource), but it does reference the Message
resource that it is targeting.
interface MessageReviewReport { id: string; messageId: string; ❶ reason: string; }
❶ This field contains the ID of a Message resource, acting as a reference.
The existence of this resource (and the fact that it’s a messageId
field is a reference to a Message
resource) leads to a couple of obvious questions. First, when you duplicate a Message
, should it also duplicate all MessageReviewReport
resources that reference the original message? And second, if a Message
resource is moved, should all MessageReviewReport
resources that reference the newly moved resource be updated as well? These questions get even more complicated when we remember that copying and moving Message
resources may not be due specifically to an end-user request targeted at the Message
resource but instead could be the cascading result of a request targeted at the parent ChatRoom
resource!
It should come as no surprise that there is no single right answer to what’s best to do in all of these scenarios, but let’s start with the easy ones, such as how related resources should respond to resources being moved.
In many relational database systems, there is a way to configure the system such that when certain database rows change, those changes can cascade and update rows elsewhere in the database. This is a very valuable, though resource intensive, feature that ensures you never have the database equivalent of a segmentation fault by trying to de-reference a newly invalid pointer. When it comes to moving resources inside an API, we ideally want to strive for the same result.
Unfortunately, this is simply one of those very difficult things that comes along with needing a move custom method. Not only do we need to keep track of all the resources that refer to the target being moved, we also need to keep track of the child resources being moved along with the parent and ensure that any other resources referencing those children are updated as well. As you can imagine, this is extraordinarily complex and one of the many reasons renaming or relocating resources is discouraged!
For example, following this guideline means that if we were to move a Message
resource that had a MessageReviewReport
referring to it, we would also need to update that MessageReviewReport
as well. Further, if we moved the ChatRoom
resource that was the parent to this Message
with a MessageReviewReport
, we would have to do the same thing because the Message
would be moved by virtue of being a child resource of the ChatRoom
.
Now let’s take a brief look at how we should handle related resources when it comes to the copy custom method.
While moving resources certainly creates quite a headache for related resources, is the same thing true of copying resources? It turns out that it also creates a headache, but it’s a headache of a different type entirely. Instead of having a hard technical problem to handle, we have a hard design decision. The reason for this is pretty simple: whether or not to copy a related resource along with a target resource depends quite a bit on the circumstances.
For example, a MessageReviewReport
resource is certainly important to have around, but if we duplicate a Message
resource we wouldn’t want to duplicate the report exactly as it is. Instead, perhaps it makes more sense to allow a MessageReviewReport
to reference more than one Message
resource, and rather than duplicating the report we can simply add the newly copied Message
resource to the list of those being referenced.
Other resources should simply never be duplicated. For example, when we copy a ChatRoom
resource we should never copy the User
resources that are listed as members in that resource. Ultimately, the point is that some resources will make sense to copy alongside the target while others simply won’t. It’s a far less complicated technical problem and instead will depend quite a lot on the intended behavior for the API.
Now that we’ve covered the details on maintaining referential integrity in the face of these new custom methods, let’s explore how things look when we expand that to encompass references outside our control.
While most references to resources in an API will live inside other resources in that same API, this isn’t always the case. There are many scenarios where resources will be referenced from all over the internet, in particular anything related to storing files or other unstructured data. This presents a pretty obvious problem given that this entire chapter is about moving resources and breaking those external references from all over. For example, let’s imagine that we have a storage system that keeps track of files. These File
resources are clearly able to be shared all over the internet, which means we’ll have lots of references to these resources all over the place! What can we do?
abstract class FileApi { @post("/files") CreateFile(req: CreateFileRequest): File; @get("/{id=files/*}") ❶ GetFile(req: GetFileRequest): File; } interface File { id: string; content: Uint8Array; }
❶ These URIs might be used to reference a given File resource from outside the API.
It’s important to remember that the internet is not exactly a perfect representation of referential integrity. Most of us encounter HTTP 404 Not Found
errors all the time, so it’s not quite fair to take the referential integrity requirements internal to an API and expand them to the entire internet. Very rarely when we provide a web resource to the world do we actually intend to sign a lifelong contract to continue providing that same resource with those exact same bytes and exact same name for the rest of eternity. Instead, we often provide the resource until it no longer makes sense. The point is that resources provided to the open internet are often best effort and rarely come with a lifetime guarantee.
In this case, our example File
resource is probably best categorized under this same set of guidelines: it’ll be present until it happens to be moved. This could be tomorrow due to an urgent request from the US Department of Justice, or it could be next year because the cost of storing the file didn’t make sense anymore. The point is that we should acknowledge up front that external referential integrity is not a critical goal for an API and focus on the more important issues at hand.
So far, all the resources we’ve copied or moved have been the kind you’d store in a relational database or control plane data. What about scenarios where we want to copy or move a resource that happens to point to a raw chunk of bytes, such as in our File
resource in listing 17.5? Should we also copy those bytes in our underlying storage?
This is actually a well-studied problem in computer science and shows up in many programming languages as the question of copy by value versus copy by reference of variables. When a variable is copied by value, all of the underlying data is duplicated and the newly copied variable is entirely separate from the old variable. When a variable is copied by reference, the underlying data remains in the same place and a new variable is created that happens to point to the original data. In this case, changing the data of one variable will also cause the data of the other variable to be updated.
let original = [1, 2, 3]; let copy_by_reference = copyByReference(original); ❶ let copy_by_value = copyByValue(original); copy_by_reference[1] = 'ref'; ❷ copy_by_value[1] = 'val';
❶ First we copy the original list twice, once by value and once by reference.
❷ Then we update each of the copies (results shown in figure 17.1).
Listing 17.6 shows an example of some pseudo-code that performs two copies (one by value, another by reference) and then updates the resulting data. The final values are then shown in figure 17.1. As you can see, despite having three variables, there are only two lists, and the changes to the copy_by_reference
variable are also visible in the original list.
When moving resources that point to this type of external data, the answer is simple: leave the underlying data alone. This means that while we might relocate the resource record itself, the data should remain untouched. For example, renaming a File
resource should change the home of that resource, but the underlying bytes should not be moved anywhere else.
When copying resources, the answer is clear but happens to be a bit more complicated. In general, the best solution for this type of external data is to begin by copying by reference only. After that, if the data ever happens to change, we should copy all of the underlying data and apply the changes because it can be wasteful to copy a whole bunch of bytes when we might be able to get away with copying by reference. However, we certainly cannot have changes on the duplicate resource showing up as changes on the original resource, so we must make the full copy once changes are about to be made.
This strategy, called copy on write, is quite common for storage systems, and many of them will do the heavy lifting for you under the hood. In other words, you might be able to get away with calling a storage system’s copy()
function and it will properly handle all the semantics to do a true copy by value only when the data is later altered.
In many APIs, there is some set of policy or metadata that is inherited by a child resource to a parent resource. For example, let’s imagine a world where we want to control the length of messages in different chat rooms. This length limit might vary from one room to another, so it would be an attribute set on the ChatRoom
resource that ultimately applies to the Message
child resources.
interface ChatRoom { id: string; // ... messageLengthLimit: number; ❶ }
❶ This setting is inherited by child Message resources to limit the length of the message content.
This is all well and good but gets more confusing when Message
resources are being copied from one ChatRoom
resource to another. The most common potential problem is when these different inherited restrictions happen to conflict, such as a Message
that happens to meet the length requirements of its current ChatRoom
resource being copied into another ChatRoom
with more stringent requirements, not met by the resource as it exists currently. In other words, what happens if we want to copy a Message
resource that has 140 characters into a ChatRoom
that only allows messages of 100 characters?
One option is to simply permit the resource to break the rules, so to speak, and exist inside the ChatRoom
resource despite being beyond the length limits. While this technically works, it introduces further complications as the standard update methods on this resource may begin to fail until the resource has been modified to conform to the rules of the parent. Often, this can make the destination resource effectively immutable, causing confusion and frustration for those interested in changing other aspects of the resource without modifying the content length.
Another option is to truncate or otherwise modify the incoming resource so that it adheres to the rules of the destination parent. While this is technically acceptable as well, it can be surprising to users who were not aware of the destination resource’s requirements. In particular, in these cases where you are permanently destroying data, this type of “force it to fit” solution breaks the best practices for good APIs by being unpredictable. The irreversible nature of this type of solution is yet another reason why it should be avoided.
A better option is to simply reject the incoming resource and abort the copy or move operation due to the violation of these rules. This ensures that users have the ability to decide what to do to make the operation succeed, whether that is altering the length requirement stored in the ChatRoom
resource or truncating or removing any offending Message
resources before attempting to copy or move the data.
This also applies to cases where copying is triggered by virtue of the resource being a child or related resource to the actual target. In these scenarios, a failure of any sort of validation check or problem due to any inherited metadata from the new destination parent should cause the entire operation to fail, with a clear reason for all of the failures standing in the way of the operation. The user then has the ability to inspect the results and decide whether to abandon the task at hand or retry the operation after fixing the issues reported.
As we’ve seen throughout this chapter, the key takeaway of both the copy and move methods should be that they can be far more complex and resource intensive than they look. While in some cases they can be pretty innocuous (e.g., copying a resource with no related or child resources) others can involve copying and updating hundreds or thousands of other resources in the API. This presents a pretty big problem because it’s rare that these copy or move operations happen when no one else is using the API and modifying the underlying resources. How do we ensure that these operations complete successfully in the face of a volatile data set? Further, it’s important that if an error is encountered along the way we are able to undo the work we’ve done. In short, we want to make sure that both our move and copy operations occur in the context of a transaction.
Interestingly, when it comes to the data storage layer, the copy and move operations tend to work in pretty different ways. In the case of the copy operation, we’ll make mostly queries to read data and then create new entries in the storage system based on those entries. In the case of the move operation, on the other hand, we’ll mostly update existing entries in the storage system, modify identifiers in place in order to move resources from one place to another, and update the existing resources that might reference the newly modified resources. While the ideal solution to both is the same, the problems encountered are slightly different, and, as a result, it makes more sense for us to address these two separately.
In the case of copying a resource (and its children and related resources), our focus when it comes to atomicity is about data consistency. In other words, we want to make sure that the data we end up with in our new destination is exactly the same data that existed at the time we initiated the copy operation, often referred to as a snapshot of the data. If your API is built on top of a storage system that supports point-in-time snapshots or transactions like this, then this problem is a lot more straightforward. You can simply specify the snapshot timestamp or revision identifier when reading the source data before copying it to the new location or perform the entire operation inside a single database transaction. Then, if anything happens to change in the meantime, that’s completely acceptable.
If you don’t have that luxury, there are two other options. First, you can simply acknowledge that this is not possible and that any data copied will be more of a smear of data across a stretch of time rather than a consistent snapshot from a single point in time. This is certainly inconvenient and can be potentially confusing, especially with extremely volatile data sets; however, it may be the only option available given the technology constraints and up-time requirements.
Next, we can lock the data for writing at either the API level (by disabling all API calls that modify data) or at the database level (preventing all updates to the data). While not always feasible, this method is sort of the “sledgehammer” option for ensuring consistency during these operations, as it will ensure that the data is exactly as it appeared at the time of the copy operation, specifically because the data was locked down and prohibited from all changes for the duration of the operation.
In general, this option is certainly not recommended as it basically provides an easy way to attack your API service: simply send lots and lots of copy operations. As a result, if your storage system doesn’t support point-in-time snapshots or transactional semantics, this is yet another reason to discourage supporting copying resources around the API.
Unlike copying data, moving data depends a lot more on updating resources, and therefore we have slightly different concerns. At first glance, it might seem like we really don’t have any major problems, but it turns out that data consistency is still an issue. To see why, let’s imagine that we have a MessageReviewReport
that points at the Message
we just moved. In this case, we need to update the MessageReviewReport
to point to the Message
resource’s new location. But what if someone updated the MessageReviewReport
to point to a different message in the meantime? In general, we need to be sure that related resources haven’t been changed since we last evaluated whether they needed to be updated to point to the newly moved resource.
To do this, we have similar options to our copying operation. First, the best choice is to use a consistent snapshot or database transaction to ensure that the work being done happens on a consistent view of the data. If that’s not possible, then we can lock the database or the API for writes to ensure that the data is consistent for the duration of the move operation. As we noted before, this is generally a dangerous tactic, but it may be a necessary evil.
Finally, we can simply ignore the problem and hope for the best. Unlike a copy operation, ignoring the problem with a move results in a far worse outcome than a simple smear of data over time. If we don’t attempt to get a consistent view of the data during a move we actually run the risk of undoing changes that were previously committed by other updates. For example, if a MessageReviewReport
is marked as needing to be updated due to a move and someone modifies the target of that resource in the meantime, it’s very possible the move operation will overwrite that update as though it had never been received in the first place. While this might not be catastrophic to all APIs, it’s certainly bad practice and should be avoided if at all possible.
As we’ve seen, the API definition itself is not nearly as complex as the behavior of that API, particularly when it comes to handling inherited metadata, child resources, related resources, and other aspects related to referential integrity. However, to summarize everything we’ve explored so far, listing 17.8 shows a final API definition that supports copying ChatRoom
resources (with supporting user-specified identifiers) and moving Message
resources between parents.
abstract class ChatRoomApi { @post("/{id=chatRooms/*}:copy") CopyChatRoom(req: CopyChatRoomRequest): ChatRoom; @post("/{id=chatRooms/*/messages/*}:move") MoveMessage(req: MoveMessageRequest): Message; } interface ChatRoom { id: string; title: string; // ... } interface Message { id: string; content: string; // ... } interface CopyChatRoomRequest { id: string; destinationParent: string; } interface MoveMessage { id: string; destinationId: string; }
Hopefully after seeing how complicated it is to support move and copy operations, your first thought is that these two should be avoided whenever possible. While these operations seem simple at first, it turns out that the behavioral requirements and restrictions can be exceptionally difficult to implement correctly. To make things worse, the consequences can be pretty dire, resulting in lost or corrupted data.
That said, copy and move are not equally complex and do not have equal standing in an API. In many cases, copying resources can actually be a critical piece of functionality. Moving resources, on the other hand, often only becomes necessary as the result of a mistake or poor resource layout. As a result, while even the best laid out APIs might find a great deal of value in supporting the copy method, it’s best to reevaluate the resource layout before deciding to implement the move method. Often, it turns out that a need to move resources across parents (or rename resources) is caused by poorly chosen resource identifiers or by relying on a parent–child relationship in what should have been a referential relationship.
Finally, it’s important to note that while the complexity spelled out in this chapter for the behavior of the move and copy operations might be onerous and challenging to implement, cutting corners here is far more likely to lead to nasty consequences down the road.
When copying a resource, should all child resources be copied as well? What about resources that reference the resource being duplicated?
How can we maintain referential integrity beyond the borders of our API? Should we make that guarantee?
When copying or moving data, how can we be sure that the resulting data is a true copy as intended and not a smear of data as it’s being modified by others using the API?
Imagine we’re moving a resource from one parent to another, but the parents have different security and access control policies. Which policy should apply to the moved resource, the old or the new?
As much as we’d love to require permanence of resources, it’s very likely that users will need to duplicate or relocate resources in an API.
Rather than relying on standard methods (update and create) to relocate or duplicate resources, we should use custom move and copy methods instead.
Copy and move operations should also include the same operation on child resources; however, this behavior should be considered on a case-by-case basis and references to moved resources should be kept up-to-date.
When resources address external data, API methods should clarify whether a copied resource is copy by reference or copy by value (or copy on write).
Copy and move custom methods should be as atomic as reasonably possible given the limitations of the underlying storage system.
18.191.202.45