mscharhag, Programming and Stuff;

A blog about programming and software development topics, mostly focused on Java technologies including Java EE, Spring and Grails.

Monday, 15 June, 2020

REST: Managing Many-To-Many relations

Introduction

Managing relations between multiple resources can be an essential part of an RESTful API. In this post we will see how many-to-many relationships can be managed with a REST API.

We use a simple user / group relation as an example. Let's assume users and groups are two separate resources (e.g. /users and /groups) and we want to provide a way to manage the relationship described by the following points:

  • A user can be added to multiple groups
  • A group can contain multiple users
  • Users can only be added once to a group

 

 

Many-to-Many relations can be divided into two different types:

  • Relations without additional information besides the actual relation
  • Relations that contain additional data. In our example this can be something like a group member status (e.g. a user is a moderator in one group and a simple member in another group)

In this post we will only look at the first type of relation. Relations with additional data will be covered in a future post.

Of course there is no single correct solution to this problem. The next section describes the approach I made the best experience with. After that, we will have a look at some alternative solutions.

Modeling sub-resources and GET operations

First we introduce two sub resources:

  • /users/<user-id>/groups represents the groups assigned to the user with id <user-id>
  • /groups/<group-id>/users represents the users assigned to the group with id <group-id>

Using the GET verb we can now request both collections.

Getting users assigned to a specific group:

GET /groups/<group-id>/users

Getting groups assigned to a specific user:

GET /users/<user-id>/groups

Adding and Removing users

Now we need a way to add a user to a group. We do this using the PUT verb.

Adding a user to a group:

PUT /groups/<group-id>/users/<user-id>

No request body is needed for this operation.

For example, this adds user 32 to group 21:

PUT /groups/21/users/32

Note, here we need to ask the question if adding a user to a group is idempotent. In our example this operation is idempotent: A user can only be added once to a group. Therefore, we use the PUT verb. If the assignment operation is not idempotent (e.g. a user can be added multiple times to a group) we have to use POST instead of PUT.

You can read more on idempotency and the difference between POST and PUT in my other posts.

As an alternative we can also model this operation from the /users perspective if we want.

Adding a group to a user:

PUT /users/<user-id>/groups/<group-id>

To remove a user from a group we use the DELETE verb.

Removing a user from a group:

DELETE /groups/<group-id>/users/<user-id>

For example, this removes user 32 from group 21:

DELETE /groups/21/users/32

or vice versa, from the /users side:

Removing a group from a user:

DELETE /users/<user-id>/groups/<group-id>

Note, while we perform PUT and DELETE operations on /groups/<group-id>/users/<user-id> there is no need to implement GET for this URI. GET /groups/21/users/32 would simply return the same result as GET /users/32 (as long as the user is part of the given group)

Alternative solutions

Introducing a separate /group-members resource

Another approach is to create a completely separate resource that manages the relation between users and groups.

Adding a user to a group might look like this:

POST /group-members
{
    groupId: 31,
    userId: 23
}

To get the users assigned to a given group, we can use a similar request as in our previous solution:

GET /groups/<group-id>/members

However, this time it returns a list of group-member resources.

This approach creates a bit more complexity (we add a completely new resource that might have its own identifier). However, it is especially useful if we want to add some additional information to the relation (e.g. the join-date of a user). We will have a closer look at this in a future post, when look at relations with additional data.

Managing relations as part of normal resource updates

Another approach is to use the standard update operation to manage relations. For example:

PUT /users/23
{
    "name" : "John",
    "groups" : [
        { "id" : "42" },
        { "id" : "43" }
    ]
}

While this can work fine in certain situations, I cannot recommend this approach.

Resources and relations are often changed independent from each other. Merging both operations together can cause various problems. For example, from the security perspective both operations might need different permissions. A client might be allowed to a add a user to a group but might not have permissions to update the user itself.

With a lot of relations this approach can also be very troublesome for performance. So it is typically better to provide separate operations for updating resources and relations.

Comments

  • Momo - Tuesday, 22 September, 2020

    Hi
    the article was so good but how can I make requests when I have a join table in my relation. User-->UserGroup<--Group and in the join table I just have composite key by userid and groupid?
    I can get user with it's groups but how can I set a user to a specific group via the join table in my restapi?

  • Mark - Thursday, 28 January, 2021

    Thank you for this article. Using your above example, how would you create an endpoint for "adding multiple groups to a user"? Something like PUT /users/<user-id>/groups with a content body of the group ID's?

  • mscharhag - Monday, 1 February, 2021

    Hi Mark,
    yes that is a possible way. However, PUT should replace the given resource. So in your example this would remove existing groups and add multiple new groups.
    Maybe PATCH would be a better choice.

  • Mary - Tuesday, 4 May, 2021

    If we have member uniquely identified by 2 properties : prop1, prop2 then using Approach: "Introducing a separate /group-members resource" How do we remove a member from Group? We can't use payload with DELETE endpoint in ASP.NET ryt but to pass prop1 and prop2 to identify member we have to use payload. How to deal with it?

  • wrobe - Friday, 19 November, 2021

    This article saved my day, thank you so much!

  • momo - Tuesday, 23 November, 2021

    Your article is so round, so firm, so fully packed, so easy to my eyes, thanks for writing this down, I was about to smash my knees with a sledge hammer, but a carefully written, simple and concise article saved my life, thank you, I love you.

  • Sam - Saturday, 10 June, 2023

    I had to think about it but I don't mind this first approach. With the 'add member' PUTs, you are effectively creating a resource with a known identifier - the identifier being the combination of group/user id.

    Second method is fine and rigorous and useful, if not the easiest thing to consume. Combining it with 'include' or 'embed' query params can make it easier.

    But a challenge I often run into is implementing one of these and then realising that the frontend really wants to bulk-replace all relationships, ideally in one transaction. That's quite tricky with the second approach, even if you have to use it because links are more involved. At this point I give up and resort to a custom endpoint like /batchcreate or /replace, but there's probably a better way.

Leave a reply