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.
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Setting up @data-client/rest only defines endpoints and schemas, so it needs no Data Client provider: leave how the app loads and stores data alone unless the user asks to change it.
APIs: RestEndpoint, resource. Vue projects: read <name>.vue.md instead of <name>.md when it exists.
Install @data-client/rest with the project's package manager. It ships the skills this setup uses next (data-client-rest, and data-client-schema from @data-client/endpoint); link them from the project root:
npx --no data-client-skills # pnpm exec data-client-skills, yarn data-client-skills.claude/skills/<skill>/SKILL.md.npx skills add reactive/data-client --skill data-client-rest --skill data-client-schema -y instead.AGENTS.md (create it if missing; if a CLAUDE.md exists, make sure it has an @AGENTS.md line), with the project's package manager in the command, and commit .agents/skills and .claude/skills:After adding, removing or upgrading `@data-client/*` packages, run `npx --no data-client-skills` and commit the skill links.Check package.json and imports. Several libraries can coexist: migrate each one's files with its reference. If HTTP calls exist but the library is unclear, ask the user.
| Detected | Follow |
|---|---|
axios | Axios migration, with the steps below |
fetch( calls or wrappers around fetch | Fetch migration |
ky | Ky migration |
superagent | SuperAgent migration |
got | Got migration |
| none | Step 3 |
Axios:
git status is clean (commit or ask first), so the codemod's changes can be reviewed on their own.npx @data-client/codemods axios-to-rest src/ (it reads TypeScript and skips node_modules). It turns each axios call into a call to an endpoint (usually a module-level const when the call is in a function), turns each axios.create() instance into a base endpoint class, and drops .data unless a response interceptor may have unwrapped it.format or lint --fix script, or the tool its config names (such as prettier --write, eslint --fix or biome check --write).TODO(data-client). Each comment names one thing to finish and links its guide section, which is in the reference under the same heading. Resolve it, then delete the comment.axios import or TODO(data-client) comment is left, uninstall axios.Put the project's shared request behavior in one RestEndpoint subclass and build every endpoint from it. If a migration made one (the axios codemod does, from axios.create()), add to that class instead of creating another.
import { RestEndpoint, RestGenerics } from '@data-client/rest';
export class BaseEndpoint<O extends RestGenerics = any> extends RestEndpoint<O> {
urlPrefix = 'https://api.example.com';
getHeaders(headers: HeadersInit): HeadersInit {
const token = localStorage.getItem('authToken');
return { ...headers, ...(token && { Authorization: `Bearer ${token}` }) };
}
}Override only what the app does today:
| Behavior | Override |
|---|---|
Auth, CSRF or other headers (may be async, e.g. token refresh) | getHeaders(headers) |
| Auth token from React context (Okta, Auth0) | hookifyResource(); a base class can't read context |
Cookies (axios withCredentials), other RequestInit | async getRequestInit(body) returning { ...(await super.getRequestInit(body)), credentials: 'include' }. Without await, the spread drops method, body and headers, so every mutation is sent as a GET |
Transform the response: unwrap an envelope like { "data": {...}, "meta": {...} }, convert case | process(value), e.g. return value?.data (value is null for a 204). Not axios's response.data: that is axios's wrapper, which the codemod already removed |
| Date fields | Not process(): the Entity's static schema, e.g. createdAt: Temporal.Instant.from (skill "data-client-schema") |
| Status handling, like a 401 redirect | async fetchResponse(input, init): super.fetchResponse() throws a NetworkError for any non-2xx status, so wrap it in try, check error.status in catch, and rethrow (Server error messages shows it) |
| Arrays or nested objects in the query | searchToString(searchParams), e.g. qs.stringify(searchParams, { arrayFormat: 'brackets' }) |
| Non-JSON responses (files, text) | The content option, e.g. content: 'blob'; otherwise parseResponse(response) |
Request bodies need no override: a plain object or array is sent as JSON, and anything else (FormData, URLSearchParams, a string) as-is (getRequestInit).
resource() for standard CRUD on one path (get, getList, create, update, partialUpdate, delete):
export const TodoResource = resource({
path: '/todos/:id',
schema: Todo,
Endpoint: BaseEndpoint,
});new BaseEndpoint({ ... }) for anything else (search, auth, custom actions). Type a body with a truthy value, body: {} as { email: string; password: string }, never undefined as unknown as T: body: undefined means the endpoint sends no body. A POST, PUT or PATCH that sends none needs that body: undefined, or its only argument is taken as the body and the path params go missing.
If the codebase validates responses with Zod or Yup, keep that by parsing in process() and still add an Entity schema; see "Coexisting with Zod or Yup" in the Axios migration.
schema:. Without one, responses aren't normalized or cached.More: Authentication, Django REST Framework.
reactive/data-client
e1ea9ce
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.