TypeScript And Errors
This package is strongly typed around your document and data shapes.
Generic Model And Data Types
You usually start by providing a document shape when you create a service.
interface User {
_id?: string;
name: string;
role: string;
public: boolean;
}
const userService = adapter.createModelService<User>({
modelName: 'User',
basePath: 'users',
});
That type then flows through:
ModelService<User>methodsModel<User>wrappers- response
rawanddatatypes
For model services, there are usually two useful layers of typing:
- the raw server document shape
- the wrapped client-facing
Model<T> & TDatashape indata
Selected Field Inference
Advanced methods infer narrower response shapes when select is precise enough.
const user = await userService.readAdvanced('user-1', {
select: ['name', 'role'] as const,
});
user.raw;
// Pick<User, 'name' | 'role'>
Projection styles that participate in inference:
- readonly tuple arrays such as
['name', 'role'] as const - exact string literals such as
'name' - exact projection objects such as
{ name: 1, role: 1 } as const
If the projection is too wide, the client falls back to a looser Partial<T>-style shape.
That fallback is intentional. The client only narrows types when the projection is specific enough to be trustworthy at compile time.
Overriding The Inferred Shape
You can provide an explicit result type if you want something narrower than the inferred selection.
const user = await userService.readAdvanced<{ name: string }>('user-1', {
select: { name: 1, role: 1 } as const,
});
That is most useful when:
- the server adds derived fields
- you intentionally want to hide part of the selected type at the call site
- you are bridging older code that expects a custom shape
Important Response Types
Common exported types include:
Response<TRaw, TData = TRaw>ModelResponse<T, TData = T>ListModelResponse<T, TData = T>DataResponse<T>ListDataResponse<T>ProjectionFilterQuery<T>PopulateIncludeWrapOptions
WrapOptions is used by wrapped endpoints:
interface WrapOptions {
queryParams?: Record<string, unknown>;
pathParams?: Record<string, string | number>;
}
Two practical distinctions matter a lot:
- for
ModelServicereads,rawis usually plain selected document data whiledatais usually aModel<T>wrapper - for
DataServicereads,rawanddataare usually the same plain value
Error Handling Modes
By default, service methods resolve to normalized failure objects instead of throwing.
const result = await userService.read('missing-id');
if (!result.success) {
console.log(result.status, result.message);
}
If you prefer exceptions, enable throwOnError:
const userService = adapter.createModelService<User>({
modelName: 'User',
basePath: 'users',
throwOnError: true,
});
Or per request:
await userService.read('missing-id', undefined, {
throwOnError: true,
});
In that mode, failed requests reject with ServiceError.
This gives you two consistent styles:
- result-oriented control flow with
if (!result.success) - exception-oriented control flow with
try/catch
ServiceError
ServiceError extends Error and keeps the normalized response fields:
successrawdatastatusheaders
Example:
import { ServiceError } from '@web-ts-toolkit/access-router-client';
try {
await userService.read('missing-id', undefined, { throwOnError: true });
} catch (error) {
if (error instanceof ServiceError) {
console.log(error.status);
console.log(error.message);
console.log(error.raw);
}
}
The message is extracted from structured server payloads in this order when available:
detailmessagetitle- nested entries inside
errors
That makes validation and problem-detail responses much easier to log and display than raw Axios errors.
If the response payload is not structured, the client falls back to stringifying the payload or using the underlying Axios error message.
Lazy Request Type
Service methods return a promise-like LazyRequest<T>.
interface LazyRequest<T> extends Promise<T> {
exec(): Promise<T>;
}
This matters for two reasons:
- you can force execution with
.exec() adapter.group(...)relies on the lazy request metadata attached to these objects
Treat them like promises in normal code, but remember they also carry batching metadata internally.
One Practical Rule
If you plan to batch requests with adapter.group(...), keep them as client-returned lazy requests until the group call.
This works:
const readUser = userService.read('user-1');
const countUsers = userService.count();
const [user, count] = await adapter.group(readUser, countUsers);
This does not:
const user = await userService.read('user-1');
const count = await userService.count();
await adapter.group(user, count);
Once awaited, the lazy request metadata is gone and you no longer have a batchable request object.