home / skills / reactive / data-client / data-client-schema

data-client-schema skill

/.cursor/skills/data-client-schema

This skill helps you design and compose scalable data schemas for entities, collections, and unions, enabling normalized, polymorphic, and relational data in

npx playbooks add skill reactive/data-client --skill data-client-schema

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
4.4 KB
---
name: data-client-schema
description: Define data schemas - Entity, Collection, Union, Query, pk/primary key, normalize/denormalize, relational/nested data, polymorphic types, Invalidate, Values
license: Apache 2.0
---

## 1. Defining Schemas

Define [schemas](references/schema.md) to represent the JSON returned by an endpoint. Compose these
to represent the data expected.

### Object

- [Entity](references/Entity.md) - represents a single unique object (denormalized)
- [new Union(Entity)](references/Union.md) - polymorphic objects (A | B)
- `{[key:string]: Schema}` - immutable objects
- [new Invalidate(Entity|Union)](references/Invalidate.md) - to delete an Entity

### List

- [new Collection([Schema])](references/Collection.md) - mutable/growable lists
- `[Schema]` - immutable lists
- [new All(Entity|Union)](references/All.md) - list all Entities of a kind

### Map

- `new Collection(Values(Schema))` - mutable/growable maps
- [new Values(Schema)](references/Values.md) - immutable maps

### Derived / selector pattern

- [new Query(Queryable)](references/Query.md) - memoized programmatic selectors
  ```ts
  const queryRemainingTodos = new Query(
    TodoResource.getList.schema,
    entries => entries.filter(todo => !todo.completed).length,
  );
  ```

  ```ts
  const groupTodoByUser = new Query(
    TodoResource.getList.schema,
    todos => Object.groupBy(todos, todo => todo.userId),
  );
  ```

---

## 2. Entity best practices

- Every `Entity` subclass **defines defaults** for _all_ non-optional serialised fields.
- Override `pk()` only when the primary key ≠ `id`.
- `pk()` return type is `number | 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 order: `process()` → [validate()](references/validation.md) → `pk()` → if existing: `mergeWithStore()` which calls `shouldUpdate()` and maybe `shouldReorder()` + `merge()`; metadata via `mergeMetaWithStore()`.
- Denormalize order: `createIfValid()` → [validate()](references/validation.md) → `fromJS()`.

---

## 4. **Union Types (Polymorphic Schemas)**

To define polymorphic resources (e.g., events), use [Union](references/Union.md) and a discriminator field.

```typescript
import { Union } from '@data-client/rest';

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. Best Practices & Notes

- Always set up `schema` on every resource/entity/collection for normalization
- Normalize deeply nested or relational data by defining proper schemas
- Use `Entity.schema` for client-side joins
- Use `Denormalize<>` type from rest/endpoint/graphql instead of InstanceType<>. This will handle all schemas like Unions, not just Entity.

## 6. Common Mistakes to Avoid

- Don't forget to use `fromJS()` or assign default properties for class fields
- Manually merging or 'enriching' data; instead use `Entity.schema` for client-side joins

# References

For detailed API documentation, see the [references](references/) directory:

- [Entity](references/Entity.md) - Normalized data class
- [Collection](references/Collection.md) - Mutable/growable lists
- [Union](references/Union.md) - Polymorphic schemas
- [Query](references/Query.md) - Programmatic selectors
- [Invalidate](references/Invalidate.md) - Delete entities
- [Values](references/Values.md) - Map schemas
- [All](references/All.md) - List all entities of a kind
- [Array](references/Array.md) - Immutable list schema
- [Object](references/Object.md) - Object schema
- [schema](references/schema.md) - Schema overview
- [relational-data](references/relational-data.md) - Relational data guide
- [computed-properties](references/computed-properties.md) - Computed properties guide
- [partial-entities](references/partial-entities.md) - Partial entities guide
- [side-effects](references/side-effects.md) - Side effects guide
- [sorting-client-side](references/sorting-client-side.md) - Client-side sorting guide

Overview

This skill defines client-side data schemas and patterns for normalizing, denormalizing, and querying JSON returned by APIs. It models Entities, Collections, Unions (polymorphic types), Maps/Values, Queries (memoized selectors), and lifecycle operations like primary key resolution, validation, merge, and invalidation. The goal is predictable async state handling across REST, GraphQL, SSE, and WebSocket feeds. It pairs TypeScript types with runtime schemas to keep UI state normalized and efficient.

How this skill works

You declare schema objects (Entity classes, Collection, Union, Values, All, Query, etc.) that mirror the API payload shape. During normalization the system runs Entity.process → validate → pk → mergeWithStore (calling shouldUpdate/shouldReorder/merge) and stores flattened records. Denormalization reconstructs nested objects via createIfValid → validate → fromJS. Queries run memoized selectors against normalized entries to compute derived results efficiently.

When to use it

  • Building a client cache for REST/GraphQL responses that must stay consistent across components
  • Handling deeply nested or relational API payloads where normalization improves updates and lookups
  • Modeling polymorphic resources (events/messages) that require type discrimination
  • Implementing optimistic updates, invalidation, or entity deletion flows
  • Writing selectors that depend on normalized store slices for memoized derived data

Best practices

  • Define defaults for all non-optional serialized fields on every Entity subclass to avoid undefined state
  • Override pk() only when the primary key is not id; return number|string|undefined
  • Set static schema on Entities for nested relations and client-side joins instead of manual merging
  • Use Entity.process to inject fields derived from request args or URL before validation
  • Prefer Denormalize<> types from the library to derive TypeScript types that respect Unions and complex schemas

Example use cases

  • Model a Todo app where Todo is an Entity, Todos list is a Collection, and user grouping uses a Query selector
  • Normalize a feed of polymorphic events using Union with a shared discriminator field (e.g., type)
  • Expose a Values(map) of settings keyed by name and a Collection of items for draggable lists
  • Handle SSE/WebSocket messages by normalizing incoming event payloads and merging into the store
  • Create a server-paginated resource where All lists every Entity of a kind and Collection handles page inserts

FAQ

How do I pick a primary key for Entity?

Use id by default; override pk() when the API uses another unique field. pk() must return number, string, or undefined for missing keys.

When should I use Union vs nested Entity schema?

Use Union when instances can be different concrete types (polymorphic). Use nested Entity schema when the shape is consistent but related and you want normalized child Entities.