4

Having built a RESTful API (using Laravel) for a project at work, I followed what seemed (from lots of reading) to be a the majority in terms of my approach to nested resources - by defining them in the path:

https://myapi.com/clients/{clientId}/tasks

rather than:

https://myapi.com/tasks?client={clientId}

Filtering results further than this, plus ordering etc was done using query parameters:

https://myapi.com/clients/{clientId}/tasks?orderby=title

Whilst initially this seemed like a good approach, it did become more difficult to maintain in terms of the controllers and routes. For example, for the above, I created a ClientsController, a TasksController AND a ClientsTasksController.

TasksController

public function store() {}
public function updated() {}
public function destroy() {}

TasksClientsController

public function index($client_id) {}

And the routes:

Route::resource('tasks', 'TasksController', ['except' => [
    'edit', 'index'
]]);
Route::resource('clients.tasks', 'ClientsTasksController', ['only' => [
    'index'
]]);

This application did not need many more nested resources such as this, it was relatively straightforward. However, I am now looking to create an API for existing application which has many more complex relationships. It is a league management app, EER diagram below:

enter image description here

Where I'm concerned is that I am going to need to query resources by a lot of different data - for example:

  1. teams by a specific division, season and round
  2. statistics by both team id and season id, and possibly round id also
  3. matches by season, round and division

and combinations of the above.

It therefore seems to me to make more sense to have a controller and route for each final resource, but to use query parameters rather than path variables.

For example:

https://myapi.com/matches?team_id={teamId}&season_id={seasonId}&roundId={roundId}

rather than:

https://myapi.com/seasons/{seasonId}/teams/{teamId}/matches

and the various extra controllers this would create.

However I feel this may be going against the grain in terms of what most people seem to suggest - see here for example. The second URL also seems neater to me.

Any help / recommendations would be very much appreciated.

1
  • Are you worried that you will have lots of resources and controllers? That isn't a problem in most cases, and is in fact desirable. Otherwise you are just building a dumb database query api, and you might as well be just talking directly to the DB in that case. Also don't forget that you can have resources that don't have to map to specific models in your system. For example if you need a resource that contains all matches teamA will play against teamB (that doesn't represent a single table in your DB but rather a joining) you can make that resource. Jan 10, 2017 at 14:47

1 Answer 1

4
+100

If you are designing L3 maturity API (HATEOAS) [1], clients do not build or remember URLs. The best approach in this case is to have multiple mappings to support both scenarios: fetching by navigation and query by population of URI template parameters (RFC 6570 [2]).

When fetching by navigation to provided link, the choice of path structure should be based on either the relationship between entities is aggregation or composition, i.e. whether one entity owns another or just refers it. For example, a player is top level entity, because life cycle of a player is not within bounds of life cycle of a team. Players may change teams, so it's natural to have following set of routes:

  • rel=players references list of players at /players
  • rel=self for player is players/{playerId}
  • rel=teams references list of teams at /teams
  • rel=self for team is teams/{teamId}
  • rel=seasons references list of seasons at /seasons
  • rel=self for season is /seasons/{seasonId}

All collection endpoints may accept query parameters to paginate and filter the response.

Aggregation examples

It's possible to know all teams of the player and vice versa, but the collection links include URI template parameters only if these parameter values are discoverable by client on prior navigations.

For example, every client starts with root of the API to fetch list of resources. At this moment, he discovers rel=teams, but the link cannot have query parameter to filter by players, because client does not yet know what are the player id values to substitute. The link may look at this moment only as following: /teams{?team_name} Same with the link rel=players at this moment: it may contain only filters by player name, but not by the team. It may look like following: /players{?first_name}.

However, when we navigate to /teams, we discover the link rel=players in /teams, that now contains team_id parameter, because its possible values are already discovered by client. It may look like following: /players{?team_id,first_name,last_name}.

As an alternative, we can use following routes discovered in responses of rel=self requests for players and teams:

  • rel=player_teams may list all teams for which player had ever performed: /players/{playerId}/teams. Each team has rel=self as defined above.
  • rel=team_players within team resource references list of team players via /teams/{teamId}/players. Each player has rel=self as defined above.

Composition example

If a division is defined for each season separately, then it has the same lifecycle and there's composition relationship. Same is for matches within a season. This means, it's natural to build the path to match as following:

/seasons/{seasonId}/matches/{matchId}

And navigate to matches within a division via following route:

/seasons/{seasonId}/divisions/{divisionId}/matches

or by search:

/seasons/{seasonId}/matches?division_id={divisionId}

The last two can be handled by the same controller if your REST API technology supports multiple mappings. All subpaths in this examples are also valid requests and shall return the respective information. Moreover, this example may be extended with additional aggregate routes like:

  • /seasons/{seasonId}/divisions/{divisionId}/teams - list of teams in division, in which every team resource has rel=self link as defined above.
  • /seasons/{seasonId}/statistics/players - list of player stats in season

References

  1. https://martinfowler.com/articles/richardsonMaturityModel.html
  2. https://www.rfc-editor.org/rfc/rfc6570
2
  • apologies for the delay here - I missed a notification clearly. You say 'clients do not build or remember URLs'. So why do you suggest 2 different mappings be created?
    – C Ivemy
    Jan 17, 2017 at 14:05
  • They serve different purposes, so it depends on which navigation options the API does support. You may choose one of them or leave the choice to client, thus supporting different types of UI (URI templates are for search and forms; prepopulated links are for navigation by links/button clicks). "Do not remember URLs" means the links are not hardcoded or cached between sessions: client can fetch some links and use them in subsequent requests in the same session. The notion of session exists in that case only on client, of course. Jan 18, 2017 at 16:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.