17 Copy and move

This chapter covers

  • How to use the copy and move custom methods for rearranging resources in an API
  • Choosing the right identifier (or identifier policy) for copy and move operations
  • How to handle child and other resources when copying or moving parent resources
  • Dealing with external data references for copied or moved resources
  • What level of atomicity should be expected from these new custom methods

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.

17.1 Motivation

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.

17.2 Overview

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.

Listing 17.1 Move-and-copy examples using custom methods

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).

17.3 Implementation

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?

17.3.1 Identifiers

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.

Copying

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.

Listing 17.2 Copy request interfaces

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.

Moving

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.

Listing 17.3 Move request interfaces

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.

17.3.2 Child resources

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.

Table 17.1 New and old identifiers after moving (or copying) a ChatRoom resource

Resource type

Original identifier

New identifier

ChatRoom

chatRooms/old

chatRooms/new

Message

chatRooms/old/messages/2

chatRooms/new/messages/2

Message

chatRooms/old/messages/3

chatRooms/new/messages/3

Message

chatRooms/old/messages/4

chatRooms/new/messages/4

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.

17.3.3 Related resources

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.

Listing 17.4 A MessageReviewReport resource that references a Message resource

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.

Referential integrity

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.

Related resource duplication

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.

External references

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?

Listing 17.5 File resources are likely to be referenced outside of the API

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.

17.3.4 External data

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.

Listing 17.6 Pseudo-code for copying by reference vs. by value

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.

Figure 17.1 Visualization of copying by reference vs. by value

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.

17.3.5 Inherited metadata

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.

Listing 17.7 ChatRoom resources with conditions inherited by children

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.

17.3.6 Atomicity

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.

Copying

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.

Moving

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.

17.3.7 Final API definition

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.

Listing 17.8 Final API definition

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;
}

17.4 Trade-offs

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.

17.5 Exercises

  1. When copying a resource, should all child resources be copied as well? What about resources that reference the resource being duplicated?

  2. How can we maintain referential integrity beyond the borders of our API? Should we make that guarantee?

  3. 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?

  4. 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?

Summary

  • 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.202.45