# API - Design & Usage This document tries to outline common design choices and usage patterns in our application suite. It’s a living document and should be extended whenever we have found common ground on an issue or learned from a mistake. It should not replace our existing OpenAPI specifications available on [api.aura.radio](https://api.aura.radio/). ## Nomenclature The document follows [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119). Additional nomenclature: - _Resource_: a specific type of data in our API associated with an endpoint - _Entity_: a database model in the respective service on which a resource is often based - _Payload_: the data returned for a resource - _Endpoint_: the URL under which a resource can be accessed ## Usage This section contains a few hints on how to effectively use the AURA API. ### Fetching multiple resources at once You will often find resources returning only a very limited amount of data or only references. You may wonder why that data is not simply embedded. We try to avoid embedding for reasons stated below, but we still want to make it easy to effectively fetch data from the API. Therefore, almost all list endpoints support the `?ids` query parameter that accepts a comma-separated list of IDs. Instead of serially fetching referenced resources, consider aggregating references and fetching them once instead: Do this: ```js const fetchJSON = (url) => fetch(url).then((r) => r.json()) async function getProgram() { const program = (await fetchJSON(`/steering/api/v1/program/basic/`))['results'] const timeslotIds = program.map((p) => p.timeslotId).join(',') const timeslots = (await fetchJSON(`/steering/api/v1/timeslots/?ids=${timeslotIds}`))['results'] const timeslotMap = new Map(timeslots.map((t) => [t.id, t])) return program.map((p) => ({ ...p, timeslot: timeslotMap.get(p.timeslotId) })) } ``` **Avoid** this: ```js const fetchJSON = (url) => fetch(url).then((r) => r.json()) async function getProgram() { const program = (await fetchJSON(`/steering/api/v1/program/basic/`))['results'] for (const p of program) { p.timeslot = await fetchJSON(`/steering/api/v1/timeslots/${p.timeslotId}/`) } return program } ``` This aggregation approach can be effectively combined with caching strategies to avoid unnecessary fetches or fetches of resources already in the cache. Caveats and notes: - Items are not guaranteed to be returned in order (use a map!). - Unknown IDs are skipped (again, use a map!). - Duplicate IDs are deduplicated but consider doing it client-side. - There is no explicit limit for the number of IDs, but consider: - the maximum URL length – ~2000 characters is generally safe, and - that the page size still applies – 100 records is safe for all endpoints. ### Picking/omitting fields from resources Sometimes you may only need a few of a resource’s fields. For instance, you might only need a show’s `id` and `name` field to implement an auto-complete input and everything else is irrelevant. Or you want to combine the aggregation approach above with a pagination/filter mechanism, so you only need IDs when filtering shows by page and categories, and then you fetch shows that you don’t already have cached. This can be achieved with the `pickFields` and `omitFields` query parameters that control which fields are added to the payload. Both accept a comma-separated list of field names that directly correspond to what you see in the resource payload. So if you only want the show’s `id` and `name` field you can query `/steering/api/v1/shows/?pickFields=id,name`. ```json5 // (await fetch('/steering/api/v1/shows/?pickFields=id,name').then(r => r.json())["results"] [ { id: 4, name: 'Musik' }, { id: 8, name: 'Nachrichten' }, ] ``` As the naming suggests: - `pickFields` is an allowlist and only returns the specified fields - whereas `omitFields` is a blocklist and only returns the fields not specified. These parameters work at data generation time, so for fields that are expensive to compute, they will reduce the load on your server. Caveats and notes: - You SHOULD NOT use both parameters at once. The behaviour is undefined. - Unknown fields will trigger an error response with a 400 HTTP status code. ## Design We try to follow a few ground rules for our REST-oriented APIs. ### Nesting API endpoints MUST NOT be nested except for operations on resources. These are allowed because they operate on a specific or all resource(s): - `/media-sources/reorder/` - `/timeslots/:id/move/` These are **not allowed**: - `/schedules/:scheduleId/timeslots/` - `/schedules/:scheduleId/timeslots/:timeslotId/media/` We avoid this pattern because there is not a fixed point at which one would naturally stop nesting, yet it quickly becomes unwieldy. Therefore, we just don’t nest in the first place. ### Embedding We don’t have a strict rule against embedding data from related entities in our API resources. Sometimes it’s the best or only option available. However, we tend to **avoid it**: - to make caching easier, - because there is often no good point at which to stop, - and because the provided data is often only needed by a specific client. Instead of adding more and more data from other entities to a resource, **carefully consider** exposing the information on the resource that represents the entity owning the data. ### References Fields that contain references to entities within the same service MUST be suffixed with `Id` (for 1:1 or n:1 relations) or `Ids` for (1:n or n:n relations). Most of the time the referenced resource can be determined from the naming of the field (e.g. `timeslotId` references `/timeslots/:timeslotId`). In case the field name does not mention the referenced resource, the API docs MUST mention it instead. References to other services SHOULD be hyperlinks (e.g. `https://example.org/battery/media-store/api/v2/files/:fileId`). ### Units Fields that have an associated unit like durations or file sizes SHOULD contain a unit suffix like `Seconds` or `Bytes`. They MUST contain a unit suffix if they deviate from the standard SI unit for the type of value like minutes (`Minutes`) or megabytes (`MiB`).