Skip to content
These docs describe arocapi v5, which is in beta. Install it with pnpm add arocapi@beta.

Request lifecycle

Every route runs your code in a fixed order. Knowing it tells you what each hook can rely on: an access function never sees a Hidden entity, storage is never called for a Denied request, and a transformer always sees the access result. Visibility vs Access covers the outcomes; this page covers where your code runs.

Routes come in two shapes. Record routes (/entities, /entity/:id, /files and /search) return entities and files as JSON and run your transformers. Metadata and Content routes (/entity/:id/metadata and /file/:id, GET and HEAD) enforce access and hand over to your storage backend.

flowchart TB
  request[Request] --> validate{Valid parameters?}
  validate -- no --> badRequest[400]
  validate -- yes --> visibility[Visibility filters<br/>applied in the query]
  visibility --> visible{Found and visible?}
  visible -- "no (single entity or file)" --> notVisible[404, or 401 to prompt a login]
  visible -- yes --> shape{Route}
  shape -- record --> base[Base transformer]
  base --> accessFn[Access function]
  accessFn --> attach[Access result attached]
  attach --> transformers[Your transformers, in order]
  transformers --> json[200 JSON]
  shape -- metadata or content --> guard[Access function]
  guard --> accessible{Accessible?}
  accessible -- no --> denied[401 if anonymous, else 403]
  accessible -- yes --> storage[Your storage backend]
  storage --> found{Returned a delivery?}
  found -- no --> missing[404]
  found -- yes --> bytes[302 redirect, or 200 with the bytes]

Fastify checks the path, query and body against each route’s schema before arocapi’s code runs. An invalid request, such as an entity id that isn’t a URL or a limit over 1000, gets a 400 and none of your hooks are called.

arocapi calls your entityVisibilityFilter and adds its result to the database query with AND. On /search it also calls searchVisibilityFilter and passes its result to the search backend, which applies it to the hits, the total and the facets, then reloads the hits from the database with the entity filter. Hidden entities are never loaded, so nothing after this step can see them, and totals and facets already exclude them.

For a single entity or file that isn’t found, arocapi answers 404. If you set promptLoginForHidden, it first asks isAuthenticated, and answers 401 instead when the request is anonymous and the entity exists. For /file/:id, visibility is that of the file’s entity.

For each visible entity, arocapi:

  1. Builds the base entity from the database row: id, type, name, description, licence ids, and memberOf and rootCollection as ids. This is what your access function receives, so access decisions never wait on parent lookups.
  2. Calls your access function with it, and in parallel replaces memberOf and rootCollection with { id, name }.
  3. Attaches the access result as access. Only the boolean flags and non-empty authorisation URLs are kept.
  4. Runs your entityTransformers one after another, each receiving the previous one’s output.

Record routes don’t enforce access. A Denied entity is still listed, with access.metadata or access.content set to false.

Files follow the same steps with fileTransformers: the access function is called with the file’s entity, and the file’s access keeps only content and contentAuthorizationUrl. On /search, arocapi adds the backend’s searchExtra (such as the score) after your transformers have run.

Transformers can rely on access being present:

import type { EntityTransformer } from 'arocapi';
export const withDownloadHint: EntityTransformer = (entity) => {
const { access } = entity;
return access.content ? entity : { ...entity, description: `${entity.description} (request access to download)` };
};

For /entity/:id/metadata and /file/:id, arocapi:

  1. Calls your access function with the base entity (for a file, its file entity) and checks metadata or content respectively.
  2. If that is false, the request is Denied. arocapi asks isAuthenticated and answers 401 for an Anonymous request or 403 otherwise. Without isAuthenticated, every Denied request gets 403. Your storage backend isn’t called.
  3. Otherwise calls the storage backend’s metadata or content half, once for GET or HEAD alike. It can assume the request is allowed.
  4. If the half returns null, answers 404, and if it throws, a logged 500. Otherwise sends a 302 redirect, or a 200 with the stream, the file or the serialised JSON, as described in Storage backend.

These routes don’t run transformers: their bodies are your RO-Crate metadata or file bytes, not entity JSON.

Hook Runs on Receives Can rely on
entityVisibilityFilter every route except /capabilities the request context nothing yet
searchVisibilityFilter /search the request context nothing yet
isAuthenticated refusals: Denied, and hidden with promptLoginForHidden the request context nothing yet
access every route except /capabilities the base entity and the request context the entity is visible
storage.metadata, storage.content metadata and file routes the Entity row, or the File row and its entity, including meta the entity is visible and accessible
entityTransformers /entities, /entity/:id, /search the entity with access attached the entity is visible
fileTransformers /files the file with access attached the file’s entity is visible

Every hook receives the same request context, { request, fastify }, so whatever your authentication sets on the request is available throughout.