CtrlK
BlogDocsLog inGet started
Tessl Logo

data-client-rest-setup

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

Quality

86%

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

REST Protocol Setup & Migration

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.

Step 1: Installation

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

Step 2: Detect Existing HTTP Patterns

Scan the codebase to determine what's currently used. Multiple patterns may coexist — run each applicable migration sub-procedure independently on the relevant files.

Detection Checklist

Check package.json dependencies and scan source files:

CheckPatternAction
"axios" in dependencies, or import.*from ['"]axios['"] in sourceAxiosFollow references/axios-migration.md
fetch( calls with REST-style URLs, or wrapper functions around fetchRaw fetchFollow references/fetch-migration.md
"ky" in dependencies, or import.*from ['"]ky['"]KyFollow references/ky-migration.md
"superagent" in dependenciesSuperAgentFollow references/superagent-migration.md
"got" in dependencies (rare in browser code)GotFollow references/got-migration.md
No existing HTTP library detectedFresh projectSkip to Step 3: Custom RestEndpoint Base Class

Ambiguous Detection

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:

  1. axios
  2. Raw fetch / custom fetch wrapper
  3. ky
  4. superagent
  5. Something else (please describe)
  6. Starting fresh (no migration needed)

Mixed Codebases

When multiple HTTP libraries are detected, run each sub-procedure on the relevant files. The sub-procedures are independent and don't conflict:

  1. Identify which files use which library (group by import statements)
  2. Run each applicable migration sub-procedure on its file group
  3. After all migrations, proceed to the base class setup

Migration References

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.

Step 3: Custom RestEndpoint Base Class

After installation and any migrations, offer to create a custom RestEndpoint class for the project.

Detection Checklist

Scan the existing codebase for common REST patterns to include:

  1. Base URL / API prefix: Look for hardcoded URLs like https://api.example.com or env vars like process.env.API_URL
  2. Authentication: Look for Authorization headers, tokens in localStorage/cookies, auth interceptors
  3. Content-Type handling: Check if API uses JSON, form-data, or custom content types
  4. Error handling: Look for error response patterns, status code handling
  5. Request/Response transforms: Data transformations, date parsing, case conversion
  6. Query string format: Simple params vs nested objects (may need qs library)

Base Class Template

Create 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}` }),
    };
  }
}

Common Lifecycle Overrides

Include these based on what's detected in the codebase. See RestEndpoint for full API documentation.

Authentication (async token refresh)

async getHeaders(headers: HeadersInit): Promise<HeadersInit> {
  const token = await getValidToken(); // handles refresh
  return {
    ...headers,
    Authorization: `Bearer ${token}`,
  };
}

Authentication from React context (Okta, Auth0)

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.

Custom Request Init (CSRF, credentials)

getRequestInit(body?: RequestInit['body'] | Record<string, unknown>): RequestInit {
  return {
    ...super.getRequestInit(body),
    credentials: 'include', // for cookies
    headers: {
      'X-CSRF-Token': getCsrfToken(),
    },
  };
}

Custom Response Parsing (unwrap data envelope)

process(value: any, ...args: any[]) {
  // If API wraps responses in { data: ... }
  return value.data ?? value;
}

Custom Error Handling

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;
}

Custom Search Params (using qs library)

searchToString(searchParams: Record<string, any>): string {
  return qs.stringify(searchParams, { arrayFormat: 'brackets' });
}

Custom parseResponse (handle non-JSON)

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);
}

Full Example with Multiple Overrides

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');
}

Usage After Setup

Once the base class is created, use it instead of RestEndpoint directly.

Choosing resource() vs individual endpoints

Use 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.

Coexisting with existing validation (Zod, Yup)

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.

Next Steps

  1. Define Entity classes (skill "data-client-schema") and wire them to endpoints via schema: — this is essential, not optional. Endpoints with schema: undefined bypass normalization and caching.
  2. Apply skill "data-client-rest" for resource and endpoint patterns
  3. Apply skill "data-client-react" or "data-client-vue" for hook-based usage

References

Repository
reactive/data-client
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.