Custom Endpoint Setup
This guide configures @data-client/endpoint for wrapping existing async functions. Use it once the Data Client provider is set up and the project has async operations 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 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:
tsexport 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:
tsexport 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
tsimport { 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
tsimport { 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
tsimport { 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
tsimport { 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
tsexport 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
tsconst 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
tsxuseSuspense(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
Vue projects: read <name>.vue.md instead of <name>.md when it exists.
- Endpoint - Full Endpoint API


