Interface Design
Resources are a collection of methods for a given data model. Entities and Schemas declaratively define the data model. Endpoints are the methods on that data. Both should be as declarative as possible — describing what, not how.
These concerns separate into three layers. Each changes for different reasons, is used in different contexts, and composes independently. Place new configuration or behavior in the layer whose reason to change matches.
The Three Layers
| Layer | Declares… | Changes when… | Examples |
|-------|-----------|---------------|----------|
| Schema (Entity, Collection, Query) | Data model and relationships | Data shape or identity changes | pk(), schema, merge(), process(), indexes |
| Endpoint (Endpoint, RestEndpoint, resource) | Methods on that data | API contract or cache policy changes | path, method, schema, pollFrequency, getOptimisticResponse() |
| Hook / Composable (useSuspense, useDLE, useQuery) | Data dependencies at point of use | UI interaction pattern changes | Suspense, { data, loading, error }, subscription lifecycle |
Placement Principles
If it describes data identity or relationships → Schema. Schemas are protocol-agnostic and UI-agnostic. An Entity's pk(), merge(), and nested schema work identically whether data arrives via REST, WebSocket, or controller.set().
If it describes how to obtain or mutate data → Endpoint. Declare path, method, schema, and policy — not imperative fetch logic. Endpoints are UI-agnostic: the same endpoint powers React, Vue, SSR, or imperative controller.fetch().
If it describes how the user experiences data → Hook. Loading states, error delivery, suspense, subscriptions, and reactivity adapters are UI concerns. Hooks co-locate data dependencies where data is rendered — not at the top of a tree. They are thin adapters over Controller, not duplicate cache logic.
If it's shared policy (expiry, staleness, optimistic updates) → EndpointExtraOptions. These live on endpoints as per-request policy, but are implemented by Controller/managers, not hooks.
Declarative Over Imperative
Favor declarative configuration that the framework interprets over imperative code users must write:
- Schemas: Declare
pk(),schemarelationships,merge()policy — the store handles normalization. - Endpoints: Declare
path,method,schema,optimistic— the framework handles fetching, caching, and updates. - Mutations:
ctrl.fetch(TodoResource.partialUpdate, { id }, { completed })— a single typed statement describing the action, not manual cache updates or invalidation cascades.
What Stays the Same vs. What Varies
The endpoint + args input pattern is the stable interface shared by all hooks and controller.fetch. This enables one endpoint definition to power useSuspense, useDLE, useCache, useSubscription, or imperative controller.fetch — without the endpoint knowing which UI pattern consumes it.
When adding a new capability, ask:
- Does every consumer need this? → Endpoint or schema.
- Do only some UI patterns need this? → Hook or hook option.
- Does it vary per call site? → An argument, not a property.
- Does it vary per endpoint definition? → A property or
extend()override.
Anti-patterns
- Imperative update/invalidation logic where a declarative schema or
optimisticflag suffices - Encoding UI concerns (loading, error display) into endpoint or schema definitions
- Encoding transport details (URL construction, headers) into hooks or components
- Duplicating store/cache logic in hooks instead of going through
Controller - Adding hook-specific state to
Controller— it serves all consumers (React, Vue, imperative) - Prop-drilling fetched data instead of co-locating
useSuspensewhere data is rendered