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:
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 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):
// 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):
// 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:
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:
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
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
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
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
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
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
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:
// 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
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
-
Apply skill "data-client-schema" to define Entity classes
-
Apply skill "data-client-react" or "data-client-vue" for usage
References
- Endpoint - Full Endpoint API