.cursor/skills/data-client-rest-setup/SKILL.md
Set up and migrate to @data-client/rest for REST APIs. Detects existing HTTP patterns (axios, fetch, ky, superagent, got) and migrates them. Creates custom RestEndpoint base class with common behaviors. Use when adopting @data-client/rest in a new or existing project.
npx skillsauth add reactive/data-client data-client-rest-setupInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill configures @data-client/rest for a project. It handles both fresh setup and migration from existing HTTP libraries. It should be applied after skill "data-client-setup" detects REST API patterns.
First, apply the skill "data-client-rest" for accurate implementation patterns.
Install the REST package alongside the core package:
# npm
npm install @data-client/rest
# yarn
yarn add @data-client/rest
# pnpm
pnpm add @data-client/rest
Scan the codebase to determine what's currently used. Multiple patterns may coexist — run each applicable migration sub-procedure independently on the relevant files.
Check package.json dependencies and scan source files:
| Check | Pattern | Action |
|-------|---------|--------|
| "axios" in dependencies, or import.*from ['"]axios['"] in source | Axios | Follow references/axios-migration.md |
| fetch( calls with REST-style URLs, or wrapper functions around fetch | Raw fetch | Follow references/fetch-migration.md |
| "ky" in dependencies, or import.*from ['"]ky['"] | Ky | Follow references/ky-migration.md |
| "superagent" in dependencies | SuperAgent | Follow references/superagent-migration.md |
| "got" in dependencies (rare in browser code) | Got | Follow references/got-migration.md |
| No existing HTTP library detected | Fresh project | Skip to Step 3: Custom RestEndpoint Base Class |
If you cannot confidently determine which patterns are used (e.g., no clear imports but HTTP calls exist), ask the user:
I found HTTP calls in your codebase but couldn't determine the library. Are you migrating from:
- axios
- Raw fetch / custom fetch wrapper
- ky
- superagent
- Something else (please describe)
- Starting fresh (no migration needed)
When multiple HTTP libraries are detected, run each sub-procedure on the relevant files. The sub-procedures are independent and don't conflict:
Each migration is a self-contained reference. Read only the relevant one(s) based on detection results above. After completing migrations, return here for base class setup.
After installation and any migrations, offer to create a custom RestEndpoint class for the project.
Scan the existing codebase for common REST patterns to include:
https://api.example.com or env vars like process.env.API_URLAuthorization headers, tokens in localStorage/cookies, auth interceptorsCreate a file at src/api/BaseEndpoint.ts (or similar location based on project structure):
import { RestEndpoint, RestGenerics } from '@data-client/rest';
/**
* Base RestEndpoint with project-specific defaults.
* Extend this for all REST API endpoints.
*/
export class BaseEndpoint<O extends RestGenerics = any> extends RestEndpoint<O> {
// API base URL - adjust based on detected patterns
urlPrefix = process.env.REACT_APP_API_URL ?? 'https://api.example.com';
// Add authentication headers
getHeaders(headers: HeadersInit): HeadersInit {
const token = localStorage.getItem('authToken');
return {
...headers,
...(token && { Authorization: `Bearer ${token}` }),
};
}
}
Include these based on what's detected in the codebase. See RestEndpoint for full API documentation.
async getHeaders(headers: HeadersInit): Promise<HeadersInit> {
const token = await getValidToken(); // handles refresh
return {
...headers,
Authorization: `Bearer ${token}`,
};
}
When auth tokens live in React context (not localStorage), getHeaders() on a base class cannot access them. Use hookifyResource() to inject context-derived headers into every endpoint:
import { hookifyResource, resource } from '@data-client/rest';
const ArticleResourceBase = resource({
path: '/articles/:id',
schema: Article,
Endpoint: BaseEndpoint,
});
export const ArticleResource = hookifyResource(
ArticleResourceBase,
function useInit() {
const accessToken = useContext(AuthContext);
return {
headers: { Authorization: `Bearer ${accessToken}` },
};
},
);
Usage: useSuspense(ArticleResource.useGet(), { id }) — the hook calls useInit() on every render, so the token is always fresh from context.
getRequestInit(body?: RequestInit['body'] | Record<string, unknown>): RequestInit {
return {
...super.getRequestInit(body),
credentials: 'include', // for cookies
headers: {
'X-CSRF-Token': getCsrfToken(),
},
};
}
process(value: any, ...args: any[]) {
// If API wraps responses in { data: ... }
return value.data ?? value;
}
async fetchResponse(input: RequestInfo, init: RequestInit): Promise<Response> {
const response = await super.fetchResponse(input, init);
if (response.status === 401) {
window.dispatchEvent(new CustomEvent('auth:expired'));
}
return response;
}
searchToString(searchParams: Record<string, any>): string {
return qs.stringify(searchParams, { arrayFormat: 'brackets' });
}
async parseResponse(response: Response): Promise<any> {
const contentType = response.headers.get('content-type');
if (contentType?.includes('text/csv')) {
return parseCSV(await response.text());
}
return super.parseResponse(response);
}
import { RestEndpoint, RestGenerics } from '@data-client/rest';
import qs from 'qs';
export class BaseEndpoint<O extends RestGenerics = any> extends RestEndpoint<O> {
urlPrefix = process.env.API_URL ?? 'http://localhost:3001/api';
async getHeaders(headers: HeadersInit): Promise<HeadersInit> {
const token = await getAuthToken();
return {
...headers,
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
};
}
getRequestInit(body?: RequestInit['body'] | Record<string, unknown>): RequestInit {
return {
...super.getRequestInit(body),
credentials: 'include',
};
}
searchToString(searchParams: Record<string, any>): string {
return qs.stringify(searchParams, { arrayFormat: 'brackets' });
}
process(value: any, ...args: any[]) {
return value?.data ?? value;
}
}
async function getAuthToken(): Promise<string | null> {
return localStorage.getItem('token');
}
Once the base class is created, use it instead of RestEndpoint directly.
resource() vs individual endpointsUse resource() when an API module has standard CRUD on a single path (list, get, create, update, delete). This is the common case:
import { resource } from '@data-client/rest';
import { BaseEndpoint } from './BaseEndpoint';
import { Todo } from '../schemas/Todo';
export const TodoResource = resource({
path: '/todos/:id',
schema: Todo,
Endpoint: BaseEndpoint,
});
// Provides: TodoResource.get, .getList, .create, .update, .delete, .partialUpdate
Use standalone new BaseEndpoint() for non-CRUD operations (search, auth, custom actions) or when the path doesn't match resource() conventions:
export const loginEndpoint = new BaseEndpoint({
path: '/auth/login',
method: 'POST' as const,
body: {} as { email: string; password: string },
schema: undefined,
});
Body typing: Use body: {} as BodyType (truthy value) — not undefined as unknown as BodyType. The truthy value is needed so the endpoint correctly sends a request body for POST/PUT/PATCH.
If the codebase already validates responses with Zod/Yup, prefer Entity as the source of truth for types that benefit from caching/normalization. Keep Zod only for types that don't need normalization (auth tokens, form validation types, one-off responses). See the migration reference files for detailed options.
schema: — this is essential, not optional. Endpoints with schema: undefined bypass normalization and caching.tools
Create a GitHub pull request from current working changes. Handles all git states - uncommitted changes, no branch, unpushed commits, etc. Analyzes diffs and changesets to generate a PR with filled-in template. Opens the PR in the browser when done. Use when the user asks to create a PR, open a PR, submit changes, or push for review.
tools
Migrate @data-client/rest path strings from path-to-regexp v6 to v8 syntax. Use when upgrading path-to-regexp, updating RestEndpoint.path or resource path strings, or when seeing errors about unexpected ?, +, (, or ) in paths.
development
Write, update, and format docs for public APIs - API reference, README, docstrings, usage examples, migration guides, deprecation notices
tools
Setup, install, and onboard new developers to Reactive Data Client monorepo - nvm, yarn, build, test, getting started guide