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.
70
86%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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/restScan 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, .partialUpdateUse 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.e823560
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.