CtrlK
BlogDocsLog inGet started
Tessl Logo
SkillPublic

data-client-rest

Define how the app talks to its API with @data-client/rest, including the shape of the data each call returns. Use it when adding or changing any API call or its data, changing how requests are sent or responses are read, or deciding how fetched data is stored, updated and kept fresh. Covers resource, RestEndpoint and pagination.

Quality

88%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Guide: Using @data-client/rest for Resource Modeling

This project uses @data-client/rest to define, fetch, normalize, and update RESTful resources and entities in React/TypeScript apps with type safety and automatic cache management.
Always follow these patterns when generating code that interacts with remote APIs.


1. Defining Schemas

Every resource() and RestEndpoint takes a schema that normalizes its response into the shared cache. Apply the skill "data-client-schema" before defining or changing one: Entity, Collection, Union and Query patterns and references live there, not here.

2. Resources (resource())

  • resource() creates a collection of RestEndpoints for CRUD operations on a common object
  • Required fields:
    • path: path‑to‑regexp template (typed!)
    • schema: Declarative data shape for a single item (typically Entity or Union)
  • Optional:
    • urlPrefix: Host root, if not /
    • searchParams: Type for query parameters (TS generic) in MyResource.getList
    • paginationField: Add MyResource.getList.getPage for pagination
    • optimistic: Boolean, when true all mutations will update optimistically, improving performance
    • body: Type for body parameter to MyResource.getList.push, MyResource.getList.unshift, MyResource.update, MyResource.partialUpdate
import { Entity, resource } from '@data-client/rest';
import { User } from './User';

export class Todo extends Entity {
  id = 0;
  user = User.fromJS();
  title = '';
  completed = false;
  createdAt = new Date();

  static key = 'Todo';
  static schema = {
    user: User,
    createdAt: (iso: string) => new Date(iso),
  }
}

export const TodoResource = resource({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
  schema: Todo,
  searchParams: {} as { userId?: string | number } | undefined,
  paginationField: 'page',
  nonFilterArgumentKeys: ['orderBy'],
  optimistic: true,
});

Usage

Rendering

// GET https://jsonplaceholder.typicode.com/todos/5
const todo = useSuspense(TodoResource.get, { id: 5 });
// GET https://jsonplaceholder.typicode.com/todos
const todoList = useSuspense(TodoResource.getList);
// GET https://jsonplaceholder.typicode.com/todos?userId=1
const todoListByUser = useSuspense(TodoResource.getList, { userId: 1 });

Mutations

const ctrl = useController();
// PUT https://jsonplaceholder.typicode.com/todos/5
const updateTodo = todo => ctrl.fetch(TodoResource.update, { id }, todo);
// PATCH https://jsonplaceholder.typicode.com/todos/5
const partialUpdateTodo = todo =>
  ctrl.fetch(TodoResource.partialUpdate, { id }, todo);
// POST https://jsonplaceholder.typicode.com/todos
const addTodoToStart = todo =>
  ctrl.fetch(TodoResource.getList.unshift, todo);
// POST https://jsonplaceholder.typicode.com/todos?userId=1
const addTodoToEnd = todo => ctrl.fetch(TodoResource.getList.push, { userId: 1 }, todo);
// PATCH https://jsonplaceholder.typicode.com/todos/5
const toggleStatus = (completed: boolean) => ctrl.fetch(TodoResource.getList.move, { id }, { completed });
// DELETE https://jsonplaceholder.typicode.com/todos/5
const deleteTodo = id => ctrl.fetch(TodoResource.delete, { id });
// GET https://jsonplaceholder.typicode.com/todos?userId=1&page=2
const getNextPage = (page) => ctrl.fetch(TodoResource.getList.getPage, { userId: 1, page })

For more detailed usage, apply the skill "data-client-react" or "data-client-vue".


3. Custom RestEndpoint patterns

/** Stand‑alone endpoint with custom typing */
export const getTicker = new RestEndpoint({
  urlPrefix: 'https://api.exchange.coinbase.com',
  path: '/products/:product_id/ticker',
  schema: Ticker,
  pollFrequency: 2000,
});

pollFrequency polls only while subscribed (useLive/useSubscription). Subscribers to the same endpoint and args share one poll at the fastest frequency, so a faster .extend({ pollFrequency }) on one view speeds up the others too.

Typing tips

  • path path‑to‑regexp template for 1st arg
  • method ≠ GET ⇒ 2nd arg = body (unless body: undefined)
  • Provide searchParams / body values purely for type inference
  • Use RestGenerics when inheriting from RestEndpoint

getOptimisticResponse()

getOptimisticResponse(snap, { id }) {
  const article = snap.get(Article, { id });
  if (!article) throw snap.abort;
  return {
    id,
    votes: article.votes + 1,
  };
}

Return each Entity's primary key plus changed fields; omitted fields keep their cached values. Without the key the UI waits for the server (dev logs Optimistic update skipped). If args lack it (PATCH /users/profile), take user from snap.getResponse(getMe).data, throw snap.abort if missing, and return { id: user.id, ...body }.


RestEndpoint lifecycle overrides

fetch() runs fetchResponse(url(), getRequestInit()) → parseResponse() → process(). Override the step that owns the change, never fetch itself (that replaces the whole lifecycle):

To changeOverride
Headers (auth)getHeaders(headers)
Other request options or body encodingasync getRequestInit(body), spreading await super.getRequestInit(body)
Query string formatsearchToString(searchParams)
The network call (retries, another client)fetchResponse(input, init)
Non-JSON responsescontent option ('blob', 'text', …), else parseResponse(response)
Shape of the parsed dataprocess(value, ...args)

4. Extending Resources

Use .extend() to add or override endpoints.

export const IssueResource = resource({
  // ...base config...
}).extend((Base) => ({
  search: Base.getList.extend({
    path: '/search/issues',
    // ...custom schema or params...
  }),
}));

5. Best Practices & Notes

  • When asked to browse or navigate to a web address, actual visit the address
  • Prefer RestEndpoint over resource() for defining single endpoints or when mutation endpoints don't exist
  • For blob/file downloads and other non-JSON responses, see network-transform: file download.

6. Common Mistakes to Avoid

  • Don't use resource() when mutation endpoints are not used or needed

References

Vue projects: read <name>.vue.md instead of <name>.md when it exists.

For detailed API documentation, see the references directory:

Guides (refer when user asks about these topics):

Concepts (refer when user asks about these topics):

  • expiry-policy - Cache invalidation, stale data, dataExpiryLength, errorExpiryLength
  • error-policy - Error handling, retry behavior, soft vs hard errors

ALWAYS follow these patterns and refer to the official docs for edge cases. Prioritize code generation that is idiomatic, type-safe, and leverages automatic normalization/caching via schema definitions.

Repository

reactive/data-client

Path

.agents/skills/data-client-rest/SKILL.md

Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.