Services
access-router-client exposes two service classes:
ModelService<T>for model routersDataService<T>for data routers
They share the same response normalization and lazy-request behavior, but their method sets are different.
ModelService<T>
Use ModelService<T> against a server-side model router.
Model reads usually return data as Model<T> wrappers. That means read results are both typed data and persistence-aware editing objects.
Standard model methods
list(args?, options?, axiosRequestConfig?)read(id, options?, axiosRequestConfig?)new(axiosRequestConfig?)create(data, options?, axiosRequestConfig?)update(id, data, options?, axiosRequestConfig?)upsert(data, options?, axiosRequestConfig?)delete(id, axiosRequestConfig?)distinct(field, axiosRequestConfig?)distinctAdvanced(field, filter, axiosRequestConfig?)count(axiosRequestConfig?)countAdvanced(filter, axiosRequestConfig?)
These methods map closely to the server-side model router operations.
create(...) and createAdvanced(...) preserve input cardinality: one object
returns ModelResponse<T>, while an array returns ArrayModelResponse<T>.
One-item arrays still return arrays.
In broad terms:
list(...)is the simple GET-based list routelistAdvanced(...)is the richer POST-based query routeread(...)is the simple GET-by-id routereadAdvanced(...)andreadAdvancedFilter(...)are richer POST-based read routescreate(...)/update(...)/upsert(...)are the simpler mutation helperscreateAdvanced(...)/updateAdvanced(...)/upsertAdvanced(...)expose richer mutation arguments such asselect,populate, and task execution
Advanced query and mutation methods
listAdvanced(filter, args?, options?, axiosRequestConfig?)readAdvanced(id, args?, options?, axiosRequestConfig?)readAdvancedFilter(filter, args?, options?, axiosRequestConfig?)createAdvanced(data, args?, options?, axiosRequestConfig?)updateAdvanced(id, data, args?, options?, axiosRequestConfig?)upsertAdvanced(data, args?, options?, axiosRequestConfig?)
Common advanced args and options
Advanced methods can use combinations of:
selectpopulateincludesortskiplimitpagepageSizetasksskimincludePermissionsincludeCountincludeExtraHeaderspopulateAccesstryListignoreCache
The exact shape depends on the specific method, but the names mirror the access-router request contract.
Rules of thumb:
- use non-advanced methods for straightforward CRUD by id
- use advanced methods when you need projection, populate, includes, server tasks, or filter-based reads
- use
includeCountonly when you actually need total counts, since it may add work on the server side - use
includeExtraHeaderswhen the server exposes count metadata through headers rather than body metadata ignoreCachealways lives on the options argument (never on args); use{ ignoreCache: true }to bypass an existing cache entryincludePermissionsis honored byModelService<T>advanced and mutation methods (includingupdate(...)andupsert(...), which transmit it as theinclude_permissionsquery parameter).DataService<T>does not advertiseincludePermissionsat all — the server data routers do not parseinclude_permissions, so the client removed it fromDataListOptions,DataListAdvancedOptions, andDataReadOptions; data records are returned without_permissions. See theDataService<T>section below for details.
Example
const users = await userService.listAdvanced(
{ public: true },
{
select: ['name', 'role'],
sort: { name: 1 },
limit: 20,
},
{
includeCount: true,
includePermissions: true,
},
{
headers: { user: 'admin' },
},
);
Subqueries
Many model methods accept sq in their options.
That is a client-side way to embed another lazy request into a filter, so the server can resolve it as an access-router subquery.
const orgs = await orgService.listAdvanced(
{
_id: userService.readAdvancedFilter(
{ name: 'lucy2' },
undefined,
{ sq: { path: 'orgs', compact: true } },
),
},
{ select: ['name'] },
);
This works because the client replaces embedded lazy requests with the special $$sq root-query metadata expected by the server.
That gives you a way to express server-side dependent queries without manually constructing the low-level root-router payload.
Subdocument Helpers
ModelService<T> also exposes subdocument helpers from id(id):
const statusHistory = userService.id(userId).subs('statusHistory');
Available methods:
list(axiosRequestConfig?)listAdvanced(filter?, args?, axiosRequestConfig?)read(subId, axiosRequestConfig?)readAdvanced(subId, args?, axiosRequestConfig?)create(data | data[], axiosRequestConfig?)update(subId, data, axiosRequestConfig?)bulkUpdate(data[], options?, axiosRequestConfig?)delete(subId, axiosRequestConfig?)
Also available:
id(id).fetch(args?, options?, axiosRequestConfig?)as a convenience alias forreadAdvanced(id, ...)
These helpers are only as capable as the matching subdocument operations exposed by the server router. If the server did not enable subs.someField.create, the client helper exists but the request will still be rejected by the server.
Return shape
Subdocument helpers return plain data, not Model<S> instances.
list(...),listAdvanced(...),create(...), andbulkUpdate(...)return aSubDocumentListResponse<S>: after narrowing onsuccess,rawanddataare the plain array of subdocument objects andcountis the array length. The sibling server never emitstotalCounton subdocument list results, soSubDocumentListResponse<S>does not carrytotalCount(usecounton successful results instead).read(...)andreadAdvanced(...)return aSubDocumentResponse<S>:rawanddataare the plain single subdocument object on success, ornullon failure.
Subdocuments are deliberately not wrapped as Model<S>: a Model<S> returned here would expose save(), which would target the parent model route (/:parentId) with the subdocument _id instead of the correct subdocument route (/:parentId/:sub/:subId). To persist a subdocument, always call the parent-scoped helper explicitly:
await statusHistory.update(subId, { label: 'processed' });
await statusHistory.create({ label: 'queued', flag: 'orange' });
await statusHistory.bulkUpdate([{ _id: subId, label: 'processed' }]);
await statusHistory.delete(subId);
create(...) accepts a single object or an array of objects (the server's subMutationBodySchema is z.union([record, array(record)])). It always returns the post-create subdocument array — the server responds with the full subdocument list, and the client normalizes the count === 1 case to [newDoc] for one consistent shape.
Example
const statusHistory = userService.id(userId).subs('statusHistory');
// `created.data` is the plain post-create array (e.g. all entries on the
// parent). `created.raw` is the same array; `created.count` is its length.
const created = await statusHistory.create({
label: 'queued',
flag: 'orange',
});
const newDoc = created.data[created.data.length - 1];
// `create(...)` also accepts an array when adding multiple rows at once:
const createdMany = await statusHistory.create([
{ label: 'queued', flag: 'orange' },
{ label: 'in-review', flag: 'blue' },
]);
const added = createdMany.data.slice(-2);
// `listed.data` is the plain array of subdocuments; `listed.count` mirrors
// the server's `count` field (not `totalCount`).
const listed = await statusHistory.list();
// `bulkUpdated.data` is the plain updated array.
const bulkUpdated = await statusHistory.bulkUpdate([
{ _id: 'sub-1', label: 'approved', flag: 'green' },
{ _id: 'sub-2', label: 'rejected', flag: 'red' },
]);
// `read.data` is the plain single subdocument (or null on 404).
const read = await statusHistory.read('sub-1');
DataService<T>
Use DataService<T> against a server-side data router.
Supported methods:
list(args?, options?, axiosRequestConfig?)listAdvanced(filter, args?, options?, axiosRequestConfig?)read(id, options?, axiosRequestConfig?)readAdvanced(id, args?, options?, axiosRequestConfig?)readAdvancedFilter(filter, args?, options?, axiosRequestConfig?)
Unlike ModelService<T>, DataService<T> is read-only from the client’s point of view.
It returns plain data objects rather than Model<T> wrappers.
Use it when the server data source is not a Mongoose model router and does not need client-side persistence helpers.
Advanced read options
For readAdvanced(...) and readAdvancedFilter(...):
ignoreCachelives on the options argument (DataReadAdvancedOptions), not on the args. Use{ ignoreCache: true }in the options position to bypass an existing cache entry. This matches the placement used bylist,listAdvanced, andread.includePermissionsis intentionally absent. The access-router data router body schema for advanced reads rejects theoptionskey, and the root router dropsitem.optionsfor data operations, so advertising it here was a type-level promise the server cannot honor. Direct and grouped advanced reads compose identical payloads as a result.
Example
const fruits = await fruitService.listAdvanced(
{ public: true },
{ select: ['id', 'name'], limit: 10 },
{ includeCount: true },
);
const apple = await fruitService.readAdvanced('apple', { select: ['name'] });
// Cache-bypass on an advanced read uses the options position:
const freshApple = await fruitService.readAdvanced(
'apple',
{ select: ['name'] },
{ ignoreCache: true },
);
Request Config And Errors
Every service method accepts an Axios request config as its last argument.
Common patterns:
- pass
headersfor auth or request-scoped permissions - pass
throwOnError: trueto convert a failed normalized response intoServiceError - pass
ignoreCache: truein supported method options when you need a fresh read
Request-scoped permissions are especially common with access-router setups. For example, if the server derives permissions from req.headers.user, the same header needs to be sent from the client for reads, writes, and grouped requests.
Example:
const user = await userService.read('user-1', undefined, {
headers: { user: 'admin' },
throwOnError: true,
});