How can I handle many-to-many relationships in a RESTful API?

Viewed 116864

Imagine you have two entities, Player and Team, where players can be on multiple teams. In my data model, I have a table for each entity, and a join table to maintain the relationships. Hibernate is fine at handling this, but how might I expose this relationship in a RESTful API?

I can think of a couple of ways. First, I might have each entity contain a list of the other, so a Player object would have a list of Teams it belongs to, and each Team object would have a list of Players that belong to it. So to add a Player to a Team, you would just POST the player's representation to an endpoint, something like POST /player or POST /team with the appropriate object as the payload of the request. This seems the most "RESTful" to me, but feels a little weird.

/api/team/0:

{
    name: 'Boston Celtics',
    logo: '/img/Celtics.png',
    players: [
        '/api/player/20',
        '/api/player/5',
        '/api/player/34'
    ]
}

/api/player/20:

{
    pk: 20,
    name: 'Ray Allen',
    birth: '1975-07-20T02:00:00Z',
    team: '/api/team/0'
}

The other way I can think of to do this would be to expose the relationship as a resource in its own right. So to see a list of all the players on a given team, you might do a GET /playerteam/team/{id} or something like that and get back a list of PlayerTeam entities. To add a player to a team, POST /playerteam with an appropriately built PlayerTeam entity as the payload.

/api/team/0:

{
    name: 'Boston Celtics',
    logo: '/img/Celtics.png'
}

/api/player/20:

{
    pk: 20,
    name: 'Ray Allen',
    birth: '1975-07-20T02:00:00Z',
    team: '/api/team/0'
}

/api/player/team/0/:

[
    '/api/player/20',
    '/api/player/5',
    '/api/player/34'
]

What is the best practice for this?

7 Answers

My preferred solution is to create three resources: Players, Teams and TeamsPlayers.

So, to get all the players of a team, just go to Teams resource and get all its players by calling GET /Teams/{teamId}/Players.

On the other hand, to get all the teams a player has played, get the Teams resource within the Players. Call GET /Players/{playerId}/Teams.

And, to get the many-to-many relationship call GET /Players/{playerId}/TeamsPlayers or GET /Teams/{teamId}/TeamsPlayers.

Note that, in this solution, when you call GET /Players/{playerId}/Teams, you get an array of Teams resources, that is exactly the same resource you get when you call GET /Teams/{teamId}. The reverse follows the same principle, you get an array of Players resources when call GET /Teams/{teamId}/Players.

In either calls, no information about the relationship is returned. For example, no contractStartDate is returned, because the resource returned has no info about the relationship, only about its own resource.

To deal with the n-n relationship, call either GET /Players/{playerId}/TeamsPlayers or GET /Teams/{teamId}/TeamsPlayers. These calls return the exactly resource, TeamsPlayers.

This TeamsPlayers resource has id, playerId, teamId attributes, as well as some others to describe the relationship. Also, it has the methods necessary to deal with them. GET, POST, PUT, DELETE etc that will return, include, update, remove the relationship resource.

The TeamsPlayers resource implements some queries, like GET /TeamsPlayers?player={playerId} to return all TeamsPlayers relationships the player identified by {playerId} has. Following the same idea, use GET /TeamsPlayers?team={teamId} to return all the TeamsPlayers that have played in the {teamId} team. In either GET call, the resource TeamsPlayers is returned. All the data related to the relationship is returned.

When calling GET /Players/{playerId}/Teams (or GET /Teams/{teamId}/Players), the resource Players (or Teams) calls TeamsPlayers to return the related teams (or players) using a query filter.

GET /Players/{playerId}/Teams works like this:

  1. Find all TeamsPlayers that the player has id = playerId. (GET /TeamsPlayers?player={playerId})
  2. Loop the returned TeamsPlayers
  3. Using the teamId obtained from TeamsPlayers, call GET /Teams/{teamId} and store the returned data
  4. After the loop finishes. Return all teams that were got in the loop.

You can use the same algorithm to get all players from a team, when calling GET /Teams/{teamId}/Players, but exchanging teams and players.

My resources would look like this:

/api/Teams/1:
{
    id: 1
    name: 'Vasco da Gama',
    logo: '/img/Vascao.png',
}

/api/Players/10:
{
    id: 10,
    name: 'Roberto Dinamite',
    birth: '1954-04-13T00:00:00Z',
}

/api/TeamsPlayers/100
{
    id: 100,
    playerId: 10,
    teamId: 1,
    contractStartDate: '1971-11-25T00:00:00Z',
}

This solution relies on REST resources only. Although some extra calls may be necessary to get data from players, teams or their relationship, all HTTP methods are easily implemented. POST, PUT, DELETE are simple and straightforward.

Whenever a relationship is created, updated or deleted, both Players and Teams resources are automatically updated.

Related