@web-ts-toolkit/access-router-client
Typed Axios-based client utilities for @web-ts-toolkit/access-router model routers, data routers, and root batch routes.
This package is designed to mirror the request contract exposed by @web-ts-toolkit/access-router:
- model routers become
ModelService<T>instances - data routers become
DataService<T>instances - root-router batching becomes
adapter.group(...) - model responses become mutable
Model<T>wrappers withsave(),reset(), and dirty tracking
Supported Runtimes
The package is officially supported in Node 22+ and modern evergreen browsers (Chrome 94+, Edge 94+, Firefox 93+, Safari 16+):
package.jsonengines.node: ">=22"— npm/pnpm warn or refuse on older Nodepackage.jsonbrowserslist: ["chrome >= 94", "edge >= 94", "firefox >= 93", "safari >= 16"]— bundlers narrow to the same browser floortsup.config.tsships the bundle at thees2022syntax intersection of both runtimes; the source imports no Node built-ins, so the samedist/index.mjsanddist/index.jsrun in either environmentwithCredentials: trueis the adapter default; in the browser this permits cookie credentials when CORS and cookie policy allow them. Authorization, API-key style headers, and NodeCookieheaders are explicit Axios config values; see Cache Controls for credentialed cache partitioning. The cache'ssetTimeout/clearTimeoutand feature-detectedunref()guard work in both runtimes.pnpm --filter @web-ts-toolkit/access-router-client test:browser-smokeruns a jsdom + Vite smoke test against the builtdist/index.mjsand fails if a Node built-in leaks into the bundle. This is a browser-like smoke check, not a real-browser engine/version compatibility gate.
Relationship To The Server
access-router-client is not a generic REST SDK generator.
It assumes the server follows the conventions from @web-ts-toolkit/access-router, including:
- model routes mounted at a known
basePath - data routes mounted at a known
basePath - advanced query routes mounted under a query segment such as
__query - advanced mutation routes mounted under a mutation segment such as
__mutation - optional root batching mounted under a root route such as
/api/root
If the server uses custom route segments, configure the client to match them exactly.
Installation
- npm
- Yarn
- pnpm
- Bun
npm install @web-ts-toolkit/access-router-client
yarn add @web-ts-toolkit/access-router-client
pnpm add @web-ts-toolkit/access-router-client
bun add @web-ts-toolkit/access-router-client
pnpm add @web-ts-toolkit/access-router-client
Unreleased Migration
The remediation release changes several consumer-visible contracts:
- subdocuments are plain data; use parent-scoped helper mutations and read
SubDocumentListResponse.count, notModel.save()ortotalCount - subdocument create always returns an array; model create preserves object versus array input cardinality
Responseis discriminated bysuccess; failuredataisnull- caching remains off at
cacheTTL: 0; enabled caches are supported-GET-only, credential-partitioned, and bounded to 100 LRU entries by default - each lazy request can execute directly or in one group, never both; grouped
requests require one effective
throwOnErrorpolicy - data services no longer accept permission options or non-string sorts, and
countAdvancediscountAdvanced(filter, config?) - strict filters require deliberate
DottedPathFilter/ServerSideCastescape hatches for dynamic paths or server-side casting - dynamic path values are encoded once, caller configs remain immutable, and
missing persistence identity on an existing projected model throws
MissingPersistenceIdentityErrorinstead of creating a duplicate
See the installed package README and repository CHANGELOG.md for the full
before/after migration table.
What It Exposes
Main entrypoint:
createAdapter(...)ModelServiceDataServiceModel- response and query helper types from
./types
Quick Start
import { createAdapter } from '@web-ts-toolkit/access-router-client';
interface User {
_id?: string;
name: string;
role: string;
public: boolean;
}
const adapter = createAdapter({
baseURL: 'http://localhost:3000/api',
});
const userService = adapter.createModelService<User>({
modelName: 'User',
basePath: 'users',
});
const listResponse = await userService.listAdvanced(
{ role: 'admin' },
{ select: ['name', 'role'], limit: 10 },
{ includeCount: true },
);
const user = await userService.read('user-id-1');
if (user.success) {
user.data.role = 'owner';
await user.data.save();
}
const grouped = await adapter.group(
userService.readAdvanced('user-id-1', { select: ['name'] }),
userService.countAdvanced({ role: 'admin' }),
);
Typical Workflow
In practice, a common client flow looks like this:
- create one adapter per API origin
- create one service per router you care about
- read documents into
Model<T>wrappers - mutate the wrapper locally
- persist with
save()or call explicit service methods - use
group(...)when severalaccess-routerrequests should share one round trip
Core Concepts
Lazy requests
Service methods return promise-like lazy requests.
- the request does not execute until you
await,.then(),.catch(),.finally(), or call.exec() - lazy requests carry internal metadata that
adapter.group(...)uses to build a root batch request - grouped requests must come from this client package, not from raw Axios calls
Response shape
Most service methods resolve to a discriminated response union:
interface SuccessResult<TRaw, TData = TRaw> {
success: true;
raw: TRaw;
data: TData;
message: string;
status: number;
headers: Record<string, string>;
}
interface FailureResult<TError = unknown> {
success: false;
raw: TError | null;
data: null;
message: string;
status: number;
headers: Record<string, string>;
}
type Response<TRaw, TData = TRaw, TError = unknown> = SuccessResult<TRaw, TData> | FailureResult<TError>;
Common conventions:
success === truemeans the HTTP request completed and the router operation succeeded- on the
success: truebranch,rawanddataare non-null - on the
success: falsebranch,datais alwaysnull;rawisunknownby default or an opt-inTErrorpayload, andmessageis extracted from structured problem payloads when possible - branch on
result.successto narrowraw/datato their successful shapes or to the documented error payload rawholds the original payload after client-side normalizationdataholds higher-level client objects such asModel<T>wrappers for model reads
For list-style responses:
totalCountis present on model list response types (ListModelResponse<T>); for subdocument list responses (SubDocumentListResponse<S>), the sibling server emitscountinstead, and that type carriescount(nottotalCount)- model/data list failures initialize
totalCount: 0; subdocument callers should narrow onsuccessbefore reading the successful list'scount - when the server returns count metadata, the client normalizes it into the appropriate field
- when count metadata is not requested, the count field may be
0or a fallback based on the route shape
Model<T> wrappers
Model reads and writes return Model<T> instances instead of plain objects.
That wrapper provides:
- direct property access like
user.data.namefor non-reserved field names assign(...),get(...), andset(...)isDirty(...)andmarkModified(...)save()for create-or-update persistencereset()to restore the last loaded or persisted snapshottoObject()/toJSON()for safe cloning and serialization
Fields named like wrapper methods (save, reset, set, get, assign, toJSON, etc.) are reserved on direct property access. Use get(...), set(...), assign(...), or toObject() for those data fields. Overlapping save() calls on the same wrapper are serialized in call order.
Package Guide
- Adapter And Setup: configuring
createAdapter(...), batching, wrapping arbitrary endpoints, and cache behavior - Services:
ModelServiceandDataServicemethods, defaults, subqueries, and subdocuments - Model: dirty tracking, save/reset behavior, path-based updates, and collision handling
- TypeScript And Errors: typed selects, response typing, and
ServiceError
Routing Notes
- model and data service requests use the router paths you configure in
basePath - grouped
adapter.group(...)requests target the root router path, which defaults toroot - if your root router uses another path, pass
rootRouterPathas the secondcreateAdapter(...)argument - custom query or mutation route segments must match the server-side
queryRouteSegmentand mutation route configuration
Common path mapping
Typical server/client alignment looks like this:
// server
runtime.createRouter('User', {
basePath: '/api/users',
queryRouteSegment: '__query',
});
// client
const adapter = createAdapter({ baseURL: 'http://localhost:3000/api' });
const userService = adapter.createModelService({
modelName: 'User',
basePath: 'users',
queryPath: '__query',
});
The client basePath is relative to the adapter baseURL, not the full server path.
When To Use It
Use access-router-client when you want:
- a typed client over
access-routermodel or data routes - model instances that can be mutated locally and persisted with
save() - root batched requests through
adapter.group(...) - a consistent error contract without hand-writing Axios wrappers
If you only need HTTP requests and do not use access-router, plain Axios is usually simpler.