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:

  • 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).