1. Defining Schemas
Define schemas to represent the JSON returned by an endpoint. Compose these to represent the data expected.
Object
- Entity - represents a single unique object (denormalized)
- EntityMixin - turn any pre-existing class into an Entity
- new Union(Entity) - polymorphic objects (A | B)
{[key:string]: Schema}- immutable objects- new Invalidate(Entity|Union) - to delete an Entity
- new Lazy(() => Schema) - break circular imports / defer deep recursive denormalization
List
- new Collection([Schema]) - mutable/growable lists
[Schema]- immutable lists- new All(Entity|Union) - list all Entities of a kind
Map
new Collection(Values(Schema))- mutable/growable maps- new Values(Schema) - immutable maps
Lens-dependent entity fields
- new Scalar({ lens, key, entity? }) - fields that vary by runtime lens (portfolio, currency, locale) without entity mutation
Derived / selector pattern
-
new Query(Queryable) - memoized programmatic selectors
const queryRemainingTodos = new Query( TodoResource.getList.schema, entries => entries.filter(todo => !todo.completed).length, );const groupTodoByUser = new Query( TodoResource.getList.schema, todos => Object.groupBy(todos, todo => todo.userId), );
2. Entity best practices
- Every
Entitysubclass defines defaults for all non-optional serialised fields. - Override
pk()only when the primary key ≠id. pk()return type isnumber | string | undefined- Override
Entity.process(value, parent, key, args)to insert fields based on args/url static schema(optional) for nested schemas or deserialization functions- When designing APIs, prefer nesting entities
3. Entity lifecycle methods
- Normalize (JSON response → cache): operates on POJOs; output is JSON-serializable plain data stored in the normalized cache. Order:
process()→pk()→ validate() → visit nested schemas (recurse intoschemafields) → if existing:mergeWithStore()which callsshouldUpdate()and maybeshouldReorder()+merge(); metadata viamergeMetaWithStore(). - Denormalize (cache → component): creates Entity class instances via
fromJS(), restoring prototype chain so getters, methods, andschemaprocessing work. Order:createIfValid()→ validate() →fromJS()→ unvisit nested schemas (recurse intoschemafields).
4. Union Types (Polymorphic Schemas)
To define polymorphic resources (e.g., events), use Union and a discriminator field.
import { Union } from '@data-client/rest'; // also available from @data-client/endpoint
export abstract class Event extends Entity {
type: EventType = 'Issue'; // discriminator field is shared
/* ... */
}
export class PullRequestEvent extends Event { /* ... */ }
export class IssuesEvent extends Event { /* ... */ }
export const EventResource = resource({
path: '/users/:login/events/public/:id',
schema: new Union(
{
PullRequestEvent,
IssuesEvent,
// ...other event types...
},
'type', // discriminator field
),
});
5. Collections (Mutable Lists & Maps)
Collections wrap Array or Values schemas to enable mutations (add/remove/move).
pk routing
pk() uses nestKey(parent, key) when nested in an Entity and available; otherwise it uses argsKey(...args), then serializes the result. Without options, it defaults to argsKey: params => ({ ...params }), using all endpoint args as the collection key.
argsKey— derive pk from endpoint arguments (default)nestKey— derive pk from parent entity for nested shared-state collections
Define both on the same Collection to reuse one definition top-level and nested. When argsKey(args) and nestKey(parent) produce the same object shape, the top-level fetch and the nested read resolve to the same (referentially equal) array/map — push/unshift/assign/move/remove on either updates both:
const userTodos = new Collection([Todo], {
argsKey: ({ userId }: { userId?: string }) => ({ userId }),
nestKey: (parent: User) => ({ userId: parent.id }),
});
nonFilterArgumentKeys
Default createCollectionFilter uses nonFilterArgumentKeys (default: keys starting with 'order') to exclude non-filter args when matching collections. This affects which existing collections receive new items from push/unshift/assign/move.
Override as function, RegExp, or string[]:
new Collection([Todo], { nonFilterArgumentKeys: /orderBy|sortDir/ })
Extenders
All usable with ctrl.set() (local-only) or via RestEndpoint extenders (network).
| Method | Type | Description |
|--------|------|-------------|
| push | Array | Entity | Append items to end |
| unshift | Array | Entity | Prepend items to start |
| assign | Values | Merge entries into map |
| remove | Both | Remove items by value from matching collections |
| move | Both | Remove from collections matching existing state, add to collections matching new state |
| addWith(merge, filter?) | Both | Custom creation schema (used internally by push/unshift/assign) |
| moveWith(merge) | Both | Custom move schema (control insertion order, e.g., unshift merge for prepending) |
6. Supplementary Endpoints (enrich existing entities)
When an endpoint returns partial or differently-shaped data for an entity already in cache (e.g., a metadata endpoint, a stats endpoint, a lazy-load expansion endpoint), use the same Entity as the schema — don't create a wrapper entity.
See partial-entities for patterns and examples.
7. Best Practices & Notes
- Always set up
schemaon every resource/entity/collection for normalization - Normalize deeply nested or relational data by defining proper schemas
- Use
Entity.schemafor client-side joins - Use
Denormalize<>type from rest/endpoint/graphql instead of InstanceType<>. This will handle all schemas like Unions, not just Entity.
8. Common Mistakes to Avoid
- The normalized cache stores plain JSON-serializable objects (POJOs), not class instances.
- Don't forget to use
fromJS()or assign default properties for class fields — bare TS field types emit no runtime defaults, so schema inference breaks - Manually merging or 'enriching' data; instead use
Entity.schemafor client-side joins
References
For detailed API documentation, see the references directory:
- Entity - Normalized data class
- EntityMixin - Turn any class into an Entity
- Collection - Mutable/growable lists
- Union - Polymorphic schemas
- Query - Programmatic selectors
- Invalidate - Delete entities
- Lazy - Deferred / circular schemas
- Scalar - Lens-dependent entity fields
- Values - Map schemas
- All - List all entities of a kind
- Array - Immutable list schema
- Object - Object schema
- schema - Schema overview
- relational-data - Relational data guide
- computed-properties - Computed properties guide
- partial-entities - Partial entities guide
- side-effects - Side effects guide
- sorting-client-side - Client-side sorting guide