Data Fetching and Hydra Pagination

The Back-Office communicates with a Symfony / API Platform backend that returns data in Hydra JSON-LD format. List endpoints return paginated collections, but because the Opal Table component handles pagination, sorting, and filtering on the client side, the Back-Office needs to fetch all items upfront before handing them to the table. The fetchAllHydraItems helper centralizes this multi-page fetching logic so that individual stores and views do not need to implement it themselves.

The Problem It Solves

Without a shared helper, every Pinia store and list view had to:
  1. Send a first request with page=1&itemsPerPage=100.
  2. Read hydra:totalItems from the response to calculate how many pages exist.
  3. Loop through remaining pages, accumulating hydra:member arrays.
  4. Manually extract and return the combined items and total count.
This logic was duplicated across articles, pages, blocks, users, categories, tags, and redirects, making it error-prone and tedious to maintain.

The fetchAllHydraItems Helper

Location: bo/src/services/api/client.ts

Signature

export async function fetchAllHydraItems<T>(
  path: string,
  params?: Record<string, any>,
  options?: RequestOptions
): Promise<{ items: T[]; total: number }>

Parameters

ParameterTypeDescription
pathstringThe API endpoint path (e.g., '/admin/articles')
paramsRecord<string, any>Optional query parameters for filtering (e.g., { status: 'published' })
optionsRequestOptionsOptional request options passed through to the underlying get() call

Return Value

FieldTypeDescription
itemsT[]All items from all pages, concatenated into a single array
totalnumberThe total item count reported by the API (hydra:totalItems)

How It Works

  1. First request — Sends page=1&itemsPerPage=100 (merged with any caller-provided params) and reads the total count from hydra:totalItems.
  2. Remaining pages in parallel — If the total exceeds 100 items, the helper calculates how many additional pages are needed and fetches them all concurrently using Promise.all. This is significantly faster than sequential fetching for large collections.
  3. Result assembly — All hydra:member arrays are concatenated and returned alongside the total count.
The helper always requests 100 items per page, which is the maximum supported by the API. Callers should not pass page or itemsPerPage in their params — the helper manages pagination internally.

Usage in Pinia Stores

Stores use fetchAllHydraItems in their list/fetch actions, passing only the filter parameters relevant to the entity. The helper handles all pagination concerns.

Example: Articles Store

import { fetchAllHydraItems } from '@/services/api/client'
import type { Article } from '@/types/api'

// Inside the store's fetch action:
const apiParams: Record<string, unknown> = {}

if (filters?.category_id) {
  apiParams.category_id = filters.category_id
}
if (filters?.status) {
  apiParams.status = filters.status
}

const { items, total } = await fetchAllHydraItems<Article>(
  '/admin/articles',
  apiParams
)

articles.value = items
totalItems.value = total
The pattern is the same across all stores (articles, pages, blocks, users):
  1. Build a params object from the active filters.
  2. Call fetchAllHydraItems with the entity endpoint and params.
  3. Assign the returned items and total to the store’s reactive state.

Usage in List Views (Without a Store)

Some simpler entities (categories, tags, redirects) do not have dedicated Pinia stores. Their list views import fetchAllHydraItems directly.

Example: CategoryListView

import { fetchAllHydraItems } from '@/services/api/client'
import type { Category } from '@/types/api'

async function loadCategories() {
  const { items, total } = await fetchAllHydraItems<Category>(
    '/admin/categories'
  )
  items.value = items
  totalItems.value = total
}
No params are needed here because the category list is unfiltered, but you can pass filters the same way stores do if needed.

When to Use This Helper

Use fetchAllHydraItems whenever you need to display all items from a Hydra collection endpoint in an Opal Table or similar client-side paginated component. Use it when:
  • You are building a new list view backed by a Hydra collection endpoint.
  • The UI component (Opal Table) handles pagination, sorting, or filtering on the client side.
  • You need all items loaded upfront rather than fetched page by page as the user navigates.
Do not use it when:
  • You need server-side pagination (i.e., fetching one page at a time as the user navigates).
  • The endpoint does not return Hydra JSON-LD format (hydra:member / hydra:totalItems).
  • You only need a single item (use get() directly instead).

Adding a New List View: Checklist

When creating a list view for a new entity, follow this pattern:
1

Decide on state management

If the entity needs CRUD operations and shared state, create a Pinia store. If it only needs a simple list, import fetchAllHydraItems directly in the view component.
2

Build filter params

Construct a Record<string, unknown> object from the user’s active filters. Do not include page or itemsPerPage.
3

Call fetchAllHydraItems

Pass the API path (e.g., '/admin/my-entity'), your filter params, and the entity type as the generic parameter.
4

Bind to the Opal Table

Assign the returned items array to the table’s data source and total to the item count display.

API Service Functions vs. fetchAllHydraItems

The codebase has two layers for API communication:
LayerLocationPurpose
Service functionsservices/api/<entity>.tsTyped wrappers for individual CRUD operations (getArticle, createArticle, updateArticle, deleteArticle)
fetchAllHydraItemsservices/api/client.tsShared helper for fetching complete paginated collections
Service functions use strictly typed ListQueryParams for their list operations, which is appropriate for targeted single-page requests. For full-collection fetching, stores and views call fetchAllHydraItems directly, bypassing the service layer’s list functions. This keeps service function signatures clean while giving stores the flexibility to pass arbitrary filter parameters.
Do not add page or itemsPerPage to the params you pass to fetchAllHydraItems. The helper manages these internally. Passing them will cause unexpected behavior.