# Data Client Endpoint Setup

> Set up @data-client/endpoint for custom async operations. Wraps existing async functions with Endpoint for use with Data Client hooks. Use after data-client-setup detects non-REST/GraphQL async patterns.

- Skill: `reactive/data-client-endpoint-setup` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add reactive/data-client-endpoint-setup`
- Raw SKILL.md: https://api.skillmd.com/api/skills/reactive/data-client-endpoint-setup/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: reactive (https://skillmd.com/u/reactive)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/reactive/data-client-endpoint-setup

---


# Custom Endpoint Setup

This skill configures `@data-client/endpoint` for wrapping existing async functions. It should be applied after `data-client-setup` detects custom async patterns that aren't REST or GraphQL.

## Installation

Install the endpoint package alongside the core package:

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

# yarn
yarn add @data-client/endpoint

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

## When to Use

Use `@data-client/endpoint` when:
- Working with third-party SDK clients (Firebase, Supabase, AWS SDK, etc.)
- Using WebSocket connections for data fetching
- Accessing local async storage (IndexedDB, AsyncStorage)
- Any async function that doesn't fit REST or GraphQL patterns

## Wrapping Async Functions

See [Endpoint](references/Endpoint.md) for full API documentation.

### Detection

Scan for existing async functions that fetch data:
- Functions returning `Promise<T>`
- SDK client methods
- WebSocket message handlers
- IndexedDB operations

### Basic Wrapping Pattern

**Before (existing code):**
```ts
// src/api/users.ts
export async function getUser(id: string): Promise<User> {
  const response = await sdk.users.get(id);
  return response.data;
}

export async function listUsers(filters: UserFilters): Promise<User[]> {
  const response = await sdk.users.list(filters);
  return response.data;
}
```

**After (with Endpoint wrapper):**
```ts
// src/api/users.ts
import { Endpoint } from '@data-client/endpoint';
import { User } from '../schemas/User';

// Original functions (keep for reference or direct use)
async function fetchUser(id: string): Promise<User> {
  const response = await sdk.users.get(id);
  return response.data;
}

async function fetchUsers(filters: UserFilters): Promise<User[]> {
  const response = await sdk.users.list(filters);
  return response.data;
}

// Wrapped as Endpoints for use with Data Client hooks
export const getUser = new Endpoint(fetchUser, {
  schema: User,
  name: 'getUser',
});

export const listUsers = new Endpoint(fetchUsers, {
  schema: [User],
  name: 'listUsers',
});
```

## Endpoint Options

Configure based on the function's behavior:

```ts
export const getUser = new Endpoint(fetchUser, {
  // Required for normalization
  schema: User,
  
  // Unique name (important if function names get mangled in production)
  name: 'getUser',
  
  // Mark as side-effect if it modifies data
  sideEffect: true, // for mutations
  
  // Cache configuration
  dataExpiryLength: 60000, // 1 minute
  errorExpiryLength: 5000, // 5 seconds
  
  // Enable polling
  pollFrequency: 30000, // poll every 30 seconds
  
  // Optimistic updates
  getOptimisticResponse(snap, id) {
    return snap.get(User, { id });
  },
});
```

## Custom Key Function

If the default key function doesn't work for your use case:

```ts
export const searchUsers = new Endpoint(fetchSearchUsers, {
  schema: [User],
  name: 'searchUsers',
  key({ query, page }) {
    // Custom key for complex parameters
    return `searchUsers:${query}:${page}`;
  },
});
```

## Common Patterns

### Firebase/Firestore

```ts
import { Endpoint } from '@data-client/endpoint';
import { doc, getDoc, collection, getDocs } from 'firebase/firestore';
import { db } from './firebase';
import { User } from '../schemas/User';

async function fetchUser(id: string): Promise<User> {
  const docRef = doc(db, 'users', id);
  const docSnap = await getDoc(docRef);
  return { id: docSnap.id, ...docSnap.data() } as User;
}

async function fetchUsers(): Promise<User[]> {
  const querySnapshot = await getDocs(collection(db, 'users'));
  return querySnapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data(),
  })) as User[];
}

export const getUser = new Endpoint(fetchUser, {
  schema: User,
  name: 'getUser',
});

export const listUsers = new Endpoint(fetchUsers, {
  schema: [User],
  name: 'listUsers',
});
```

### Supabase

```ts
import { Endpoint } from '@data-client/endpoint';
import { supabase } from './supabase';
import { User } from '../schemas/User';

async function fetchUser(id: string): Promise<User> {
  const { data, error } = await supabase
    .from('users')
    .select('*')
    .eq('id', id)
    .single();
  if (error) throw error;
  return data;
}

async function fetchUsers(filters?: { role?: string }): Promise<User[]> {
  let query = supabase.from('users').select('*');
  if (filters?.role) {
    query = query.eq('role', filters.role);
  }
  const { data, error } = await query;
  if (error) throw error;
  return data;
}

export const getUser = new Endpoint(fetchUser, {
  schema: User,
  name: 'getUser',
});

export const listUsers = new Endpoint(fetchUsers, {
  schema: [User],
  name: 'listUsers',
});
```

### IndexedDB

```ts
import { Endpoint } from '@data-client/endpoint';
import { User } from '../schemas/User';

async function fetchUserFromCache(id: string): Promise<User | undefined> {
  const db = await openDB('myapp', 1);
  return db.get('users', id);
}

async function fetchUsersFromCache(): Promise<User[]> {
  const db = await openDB('myapp', 1);
  return db.getAll('users');
}

export const getCachedUser = new Endpoint(fetchUserFromCache, {
  schema: User,
  name: 'getCachedUser',
  dataExpiryLength: Infinity, // Never expires
});

export const listCachedUsers = new Endpoint(fetchUsersFromCache, {
  schema: [User],
  name: 'listCachedUsers',
  dataExpiryLength: Infinity,
});
```

### WebSocket Fetch

```ts
import { Endpoint } from '@data-client/endpoint';
import { socket } from './socket';
import { Message } from '../schemas/Message';

async function fetchMessages(roomId: string): Promise<Message[]> {
  return new Promise((resolve, reject) => {
    socket.emit('getMessages', { roomId }, (response: any) => {
      if (response.error) reject(response.error);
      else resolve(response.data);
    });
  });
}

export const getMessages = new Endpoint(fetchMessages, {
  schema: [Message],
  name: 'getMessages',
});
```

## Mutations with Side Effects

```ts
export const createUser = new Endpoint(
  async (userData: Omit<User, 'id'>): Promise<User> => {
    const { data, error } = await supabase
      .from('users')
      .insert(userData)
      .select()
      .single();
    if (error) throw error;
    return data;
  },
  {
    schema: User,
    name: 'createUser',
    sideEffect: true,
  },
);

export const deleteUser = new Endpoint(
  async (id: string): Promise<{ id: string }> => {
    const { error } = await supabase.from('users').delete().eq('id', id);
    if (error) throw error;
    return { id };
  },
  {
    name: 'deleteUser',
    sideEffect: true,
  },
);
```

## Using extend() for Variations

```ts
const baseUserEndpoint = new Endpoint(fetchUser, {
  schema: User,
  name: 'getUser',
});

// With different cache settings
export const getUserFresh = baseUserEndpoint.extend({
  dataExpiryLength: 0, // Always refetch
});

// With polling
export const getUserLive = baseUserEndpoint.extend({
  pollFrequency: 5000, // Poll every 5 seconds
});
```

## Important: Function Name Mangling

In production builds, function names may be mangled. **Always provide explicit `name` option**:

```ts
// Bad - name may become 'a' or similar in production
const getUser = new Endpoint(fetchUser);

// Good - explicit name survives minification
const getUser = new Endpoint(fetchUser, { name: 'getUser' });
```

## Usage in with hooks and controller

```tsx
useSuspense(getUser, id);
ctrl.fetch(createUser, userData);
```

Both hooks and controller methods take endpoint as first argument, with the endpoint's function arguments following.

## Next Steps

1. Apply skill "data-client-schema" to define Entity classes
2. Apply skill "data-client-react" or "data-client-vue" for usage

## References

- [Endpoint](references/Endpoint.md) - Full Endpoint API

