chore: refactor API endpoints' pattern SD-97
Following discussion here !434 (comment 64407), a more simplified pattern for endpoints will be used.
BE of websites/root!879 (merged)
Overview
This merge request proposes a new, simplified endpoint pattern aimed at reducing unnecessary chaining and improving overall maintainability and consistency across our API endpoints. The new pattern focuses on using the minimal set of UUIDs/primary keys in the path, only chaining endpoints when there is a clear dependency that we want to enforce.
Current Issues
- Unlimited chaining: Following a "chain-all" approach would lead us to excessive deeply nested paths that are unnecessarily long. This not only complicates the API structure but also increases the burden on the frontend to manage and pass multiple UUIDs, especially when most of them are not used in the backend service to any logic.
@Delete(':eventUUID/sessions/:sessionUUID')
@AssertStaffPrivilege(StaffPrivilegeKind.CAN_MANAGE_EVENTS)
async delete (@Param() params: EventSessionParams): Promise<void> {
await this.service.delete(params.sessionUUID)
}
The BE controller simply ignores the eventUUID
parameter of the endpoint; as sessionUUID
is the primary key needed to target the resource.
- Inconsistent Patterns: Over time, endpoint naming has become inconsistent. Different modules and endpoints have different levels of nesting, different ways of organising dependencies, making the API harder to navigate and maintain.
GET /teams/competition/{competitionUUID}
GET /teams/{teamID}/memberships
PUT /teams/{teamID}/memberships/{membershipUUID}/accept
- Frontend Complexity: The current pattern forces the frontend to handle and pass multiple UUIDs through various components, leading to increased complexity and potential for errors.
In the example above, it requires the eventUUID
to be returned by the BE in the DTO of a Session
, and the FE to pass its value down to several components, so it can issue the request.
Proposed Pattern
The proposed pattern adopts a more concise and logical approach:
- Use minimal UUIDs: Only include UUIDs that are necessary for identifying the resource.
- Limit nesting: Only nest resources endpoints when it is required to identify the resource.
- Adopt clear filtering: Following RESTful best pratices, nest paths for logical filtering.
Examples:
GET /competitions/{competitionUUID}/teams # returns collection of teams in a competition
POST /competitions/{competitionUUID}/teams # create a team, adding it to the collection
GET /teams/{teamID} # access team solely based on its ID
GET /teams/{teamID}/memberships # returns collection of memberships in a team
PUT /teams/memberships/{membershipUUID}/accept # manipulate membership solely based on its UUID
Note: Why not using
/memberships/{membershipUUID}/accept
(without/teams
preffix)? Because memberships is a BE-only module. Its use-cases (e.g., team roster, labs members, in the future: judges, staff members, etc) have each their own needs for authorisation, deadlines, etc. They will have their own endpoint each, and the logic underneath will be shared. You can see this as a class abstraction.
Other changes
-
GET / vs GET /search: In general, collection of resources should be returned paginated and allowing some level of filtering and ordering. Only for more specific endpoints (e.g., HQ or Team Selector), we can create specialised endpoints adding
/search
to their path.