home / skills / reactive / data-client / data-client-rest-setup

data-client-rest-setup skill

/.cursor/skills/data-client-rest-setup

This skill configures @data-client/rest for REST APIs, offering a project-specific BaseEndpoint class with urlPrefix, auth, and error handling.

npx playbooks add skill reactive/data-client --skill data-client-rest-setup

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.2 KB
---
name: data-client-rest-setup
description: Set up @data-client/rest for REST APIs. Creates custom RestEndpoint base class with common behaviors (auth, urlPrefix, error handling). Use after data-client-setup detects REST patterns.
disable-model-invocation: true
---

# REST Protocol Setup

This skill configures `@data-client/rest` for a project. It should be applied after skill "data-client-setup" detects REST API patterns.

**First, apply the skill "data-client-rest"** for accurate implementation patterns.

## Installation

Install the REST package alongside the core package:

```bash
# npm
npm install @data-client/rest

# yarn
yarn add @data-client/rest

# pnpm
pnpm add @data-client/rest
```

## Custom RestEndpoint Base Class

After installing, **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):

```ts
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](references/RestEndpoint.md) for full API documentation.

### Authentication (async token refresh)

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

### Custom Request Init (CSRF, credentials)

```ts
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)

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

### Custom Error Handling

```ts
async fetchResponse(input: RequestInfo, init: RequestInit): Promise<Response> {
  const response = await super.fetchResponse(input, init);
  
  // Handle specific status codes
  if (response.status === 401) {
    // Trigger logout or token refresh
    window.dispatchEvent(new CustomEvent('auth:expired'));
  }
  
  return response;
}
```

### Custom Search Params (using qs library)

```ts
searchToString(searchParams: Record<string, any>): string {
  // For complex nested query params
  return qs.stringify(searchParams, { arrayFormat: 'brackets' });
}
```

### Custom parseResponse (handle non-JSON)

```ts
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

```ts
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[]) {
    // Unwrap { data: ... } envelope if present
    return value?.data ?? value;
  }
}

// Helper function - implement based on project auth pattern
async function getAuthToken(): Promise<string | null> {
  // Check for valid token, refresh if needed
  return localStorage.getItem('token');
}
```

## Usage After Setup

Once the base class is created, use it instead of [RestEndpoint](references/RestEndpoint.md) directly:

```ts
import { BaseEndpoint } from './BaseEndpoint';
import { Todo } from '../schemas/Todo';

export const getTodo = new BaseEndpoint({
  path: '/todos/:id',
  schema: Todo,
});

export const updateTodo = getTodo.extend({ method: 'PUT' });
```

Or with `resource()`:

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

## Next Steps

1. Apply skill "data-client-schema" to define Entity classes
2. Apply skill "data-client-rest" for resource and endpoint patterns
3. Apply skill "data-client-react" or "data-client-vue" for usage

## References

- [RestEndpoint](references/RestEndpoint.md) - Full RestEndpoint API
- [resource](references/resource.md) - Resource factory function
- [Authentication Guide](references/auth.md) - Auth patterns and examples
- [Django Integration](references/django.md) - Django REST Framework patterns

Overview

This skill sets up @data-client/rest for REST APIs and creates a project-wide RestEndpoint base class with common behaviors like auth, urlPrefix, and error handling. It runs after REST patterns are detected and scaffolds a BaseEndpoint tailored to your project's URL, headers, and response conventions. The result is a single place to centralize request/response transforms and lifecycle hooks for all REST endpoints.

How this skill works

The skill installs @data-client/rest (or ensures it is present) and generates a BaseEndpoint TypeScript file that extends RestEndpoint. It inspects the codebase for base URLs, auth patterns, content types, error shapes, and query param conventions, then wires sensible defaults (urlPrefix, getHeaders, getRequestInit, process, parseResponse, searchToString). It also offers common lifecycle overrides like async token refresh, CSRF handling, and custom error handling for status codes.

When to use it

  • You have REST-style endpoints detected by a prior data-client setup step.
  • You want a centralized place to add auth, CSRF, or token refresh logic.
  • You need consistent request/response transforms across many endpoints.
  • You use special query serialization (nested objects, arrays) or non-JSON responses.
  • You plan to reuse the same header and error handling logic across resources.

Best practices

  • Keep BaseEndpoint small and focused: auth, urlPrefix, and parsing helpers only.
  • Prefer async getHeaders that handles token refresh, not ad-hoc header changes in each endpoint.
  • Unwrap API envelopes in process() so schemas and consumers receive normalized data.
  • Use searchToString with qs for nested query params when necessary.
  • Handle non-JSON responses in parseResponse and surface meaningful errors from fetchResponse.

Example use cases

  • Generate src/api/BaseEndpoint.ts with urlPrefix from process.env and Authorization header pulled from storage or a refresh helper.
  • Implement getRequestInit to include credentials: 'include' for cookie-based auth and add CSRF headers.
  • Override process() to unwrap { data: ... } envelopes before normalization with normalizr.
  • Customize searchToString to use qs.stringify({ arrayFormat: 'brackets' }) for nested query params.
  • Intercept 401 responses in fetchResponse to emit an auth:expired event or trigger a refresh workflow.

FAQ

Do I have to use the generated BaseEndpoint for every endpoint?

No, but using BaseEndpoint for all REST endpoints keeps auth, parsing, and error handling consistent and reduces duplication.

How do I handle file uploads or form-data?

Override getRequestInit to set body to FormData and omit JSON Content-Type; let the browser set the multipart header automatically.