Search code examples
jsonweb-servicesresthttprestful-architecture

Updating RESTful resources against aggregate roots only


Usually I architect RESTful APIs using the following resource URI scheme:

POST /products
PATCH /products/{id}
GET /products
GET /products/{id}
DELETE /products/{id}

Products may also contain product features. When I want to get some product's features, I would perform a GET /products/{id}/features.

BTW, if I want to add new features to a given product, usually I don't provide a resource URI like this: PATCH /products/{id}/features but I consider that features are part of a given product, thus, I update which features might contain a feature as follows:

PATCH /products/{id}

{
    "features": {
         "add": [1, 2, 3]
    }
}

In the other hand, if I want to update some feature metadata I wouldn't use the product resource but I would perform a request like this:

PATCH /products/features/{id}

{
    title: "Test"
}

In my case, product features are unrelated to a particular product, but they can be associated to many products.

Ideally, I should update which features own a given product issuing a PATCH request to /products/{id}/features, BTW it overcomplicates the server API because you need to cover all entity's aggregates separately.

My concern is if it's fine to consider that some given aggregate root's associations are updatable as part of the entity itself.

More background on the topic

At the end of the day, one might say that an API like this isn't fully RESTful, because I shouldn't expect to remove features from some given product using PATCH verb but DELETE: DELETE /products/{id}/features/{featureId}, which turns the API usage from the client point of view easier than patching the product with a DTO.


Solution

  • Right now your architecture isn't perfectly clean. In part, you implement restful way of thinking, but on the other hand, things like

    PATCH /products/{id}
    
    {
        "features": {
             "add": [1, 2, 3]
        }
    }
    

    or

    PATCH /products/features/{id}
    
    {
        title: "Test"
    }
    

    are unintuitive.

    For the first example, I would recommend

    PATCH /products/{id}
    
    {
        "features": [
            {
                "id": 1
            },
            {
                "id": 2
            },
            {
                "id": 3
            },
            {
                "id": 4
            }
        ]
    }
    

    where you provide all features for this product. You can't just define your own element add, which adds given features to the product. Structure of the updating resource should be comparable to structure you get with GET /products/{id} (I guess you don't get add attribute, do you?).

    For the second one, your url should be sth like /product-features/{feature_id} or just /features/{feature_id}. Don't break the pattern /products/{product_id} with /products/features/{feature_id}. Why should you think this way? It's logical - when you GET /products, you don't get resource with list of all features

    {
        ...
        "features": [
            {
                 "id": 1
            },
            ...
        ]
        ...
    }
    

    but instead of, a list of all products

    {
         [
              {
                   ....
                   "id": 1,
                   "features": ...
              },
              ...
         ]
    }
    

    Answering to your question, if you implement it properly as I suggested in a restful way, updating product's features is absolutely okay this method.


    Edit:

    About the thing of /products/features or /product-features, is there any consensus on this? Do you know any good source to ensure that it's not just a matter of taste?

    I think this is misleading. I would expect to get all features in all products rather than get all possible features. But, to be honest, it’s hard to find any source talking directly about this problem, but there is a bunch of articles where people don’t try to create nested resources like /products/features, but do this separately.

    About the thing of add when patching, it's the easiest way I've found to express that I'm adding, updating or removing features from a given product. In fact, it's a DTO

    If you want to use some actions on collections, there is a standard for it. Just take a look at Best practice to batch update a collection in a REST Api call and https://www.rfc-editor.org/rfc/rfc6902#appendix-A.2. Instead of

    PATCH /products/{id}
    
    {
        "features": {
             "add": [1, 2, 3]
        }
    }
    

    you will send

    PATCH /products/{id}
    
    {
        "op": add, "path": "/features/0", "value": 1,
        "op": add, "path": "/features/0", "value": 2,
        "op": add, "path": "/features/0", "value": 3
    }
    

    BTW, note that you didn't answer the core issue in my question.

    As everything can be considered as sub-resource, I don't see any conflict if product's features, as a collection, is an attribute of the product and also it's considered as sub-resource to enable POST for example. If one's find it not safe, then you can cut it to operate only on sub-resource.

    Moreover, in this article author approved the way of updating sub-resources with PATCH do simplify the process (the whole article is a little bit controversial but it's not over the thing we are interested)

    Another solution is to expose the resource’s properties you want to make editable, and use the PUT method to send an updated value. In the example below, the email property of user 123 is exposed:

    PUT /users/123/email
     
    [email protected]
    

    While it makes things clear, and it looks like a nice way to decide what to expose and what not to expose, this solution introduces a lot of complexity into your API (more actions in the controllers, routing definition, documentation, etc.). However, it is REST compliant, and a not-so-bad solution, but there is a better alternative: PATCH