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
- https://martinfowler.com/articles/richardsonMaturityModel.html
- https://tools.ietf.org/html/rfc6570