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]
Validation
Section titled “Validation”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.
Visibility
Section titled “Visibility”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.
Record routes
Section titled “Record routes”For each visible entity, arocapi:
- Builds the base entity from the database row: id, type, name, description, licence ids, and
memberOfandrootCollectionas ids. This is what youraccessfunction receives, so access decisions never wait on parent lookups. - Calls your
accessfunction with it, and in parallel replacesmemberOfandrootCollectionwith{ id, name }. - Attaches the access result as
access. Only the boolean flags and non-empty authorisation URLs are kept. - Runs your
entityTransformersone 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)` };};Metadata and Content routes
Section titled “Metadata and Content routes”For /entity/:id/metadata and /file/:id, arocapi:
- Calls your
accessfunction with the base entity (for a file, its file entity) and checksmetadataorcontentrespectively. - If that is
false, the request is Denied. arocapi asksisAuthenticatedand answers401for an Anonymous request or403otherwise. WithoutisAuthenticated, every Denied request gets403. Your storage backend isn’t called. - Otherwise calls the storage backend’s
metadataorcontenthalf, once forGETorHEADalike. It can assume the request is allowed. - If the half returns
null, answers404, and if it throws, a logged500. Otherwise sends a302redirect, or a200with 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.
Where your code runs
Section titled “Where your code runs”| 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.