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.
Nomenclature
The document follows RFC 2119.
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:
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:
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.
// (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:
pickFieldsis an allowlist and only returns the specified fieldswhereas
omitFieldsis 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).