Skip to content

Configuration Guide

Complete guide to configuring Bindra DataSources and all features.


Table of Contents


Overview

Bindra DataSources are configured through the DataSourceConfig object passed to the constructor. Configuration is flexible and can be as simple or comprehensive as needed.

typescript
import { DataSource } from 'bindra';
import type { DataSourceConfig } from 'bindra';

const config: DataSourceConfig<User> = {
  // Minimal configuration
  url: '/api/users'
};

const users = new DataSource<User>(config);

Basic Configuration

Minimal Remote DataSource

typescript
const users = new DataSource<User>({
  url: '/api/users'
});

Minimal Local DataSource

typescript
const users = new DataSource<User>({
  data: [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' }
  ]
});

URL and Data Are Mutually Exclusive

typescript
// ✅ Valid - Remote mode
new DataSource<User>({ url: '/api/users' });

// ✅ Valid - Local mode
new DataSource<User>({ data: [] });

// ❌ Invalid - Cannot specify both
new DataSource<User>({
  url: '/api/users',
  data: []  // Error: either url or data, not both
});

// ❌ Invalid - Must specify at least one
new DataSource<User>({});  // Error: url or data required

Data Modes

Local Data Mode

Use when working with client-side data (no API).

typescript
const todos = new DataSource<Todo>({
  data: [
    { id: 1, title: 'Learn Bindra', completed: false },
    { id: 2, title: 'Build app', completed: false }
  ]
});

// All operations work on local data
await todos.create({ title: 'New todo', completed: false });
await todos.update(1, { completed: true });
await todos.delete(2);

// Data is reactive
console.log(todos.data); // Updated automatically

Features in Local Mode:

  • ✅ Full CRUD operations
  • ✅ Reactive data updates
  • ✅ Navigation (next, prev, goto)
  • ✅ Client-side filtering and sorting
  • ✅ Validation
  • ❌ No network requests
  • ❌ No caching (data always in memory)
  • ❌ No real-time updates

Remote Data Mode

Use when working with a REST API.

typescript
const users = new DataSource<User>({
  url: 'https://api.example.com/users'
});

// Operations make HTTP requests
await users.fetch();           // GET /users
await users.create(newUser);   // POST /users
await users.update(1, changes); // PUT /users/1
await users.delete(1);         // DELETE /users/1

Features in Remote Mode:

  • ✅ Full CRUD operations via HTTP
  • ✅ Automatic config fetching from API
  • ✅ Response caching
  • ✅ Retry with exponential backoff
  • ✅ Real-time WebSocket updates
  • ✅ Validation
  • ✅ Security features (CSRF, sanitization)

Expected API Endpoints:

OperationMethodEndpointBody
ConfigGET/users/config-
Fetch allGET/users-
Fetch oneGET/users/:id-
CreatePOST/usersRecord
UpdatePUT/users/:idChanges
DeleteDELETE/users/:id-
Batch createPOST/users/batchRecords[]
Batch updatePUT/users/batchUpdates[]
Batch deleteDELETE/users/batchIDs[]

Pagination Configuration

Control how data is paginated.

Basic Pagination

typescript
const users = new DataSource<User>({
  url: '/api/users',
  pageSize: 20  // 20 records per page
});

// Access pagination state
const pagination = users.pagination.get();
console.log(pagination);
// {
//   pageSize: 20,
//   currentPage: 1,
//   totalRecords: 142,
//   totalPages: 8
// }

// Navigate pages
await users.fetchPage(2);     // Load page 2
await users.fetchMore();      // Load next page (append)

// Update page size
users.pagination.set({
  ...users.pagination.get(),
  pageSize: 50
});

Pagination Strategies

Bindra supports two pagination strategies based on your API:

Offset-based (Default)

Uses page and limit query parameters.

typescript
const users = new DataSource<User>({
  url: '/api/users',
  pageSize: 20
});

// Generates: GET /api/users?page=1&limit=20
await users.fetch();

// Generates: GET /api/users?page=2&limit=20
await users.fetchPage(2);

API Response Format:

json
{
  "data": [...],
  "page": 1,
  "limit": 20,
  "total": 142
}

Cursor-based

Uses cursor tokens for pagination (better for real-time data).

typescript
const messages = new DataSource<Message>({
  url: '/api/messages',
  pageSize: 50,
  pagination: {
    strategy: 'cursor',
    cursorField: 'id'  // Field used for cursor
  }
});

// First fetch: GET /api/messages?limit=50
await messages.fetch();

// Next fetch: GET /api/messages?after=<last_id>&limit=50
await messages.fetchMore();

API Response Format:

json
{
  "data": [...],
  "cursor": {
    "next": "cursor_token_here",
    "hasMore": true
  }
}

Infinite Scroll

typescript
const products = new DataSource<Product>({
  url: '/api/products',
  pageSize: 20
});

// Initial load
await products.fetch();

// Load more on scroll
window.addEventListener('scroll', async () => {
  if (isNearBottom() && products.hasMore) {
    await products.fetchMore();
  }
});

Caching Configuration

Control response caching to reduce API calls.

Enable Caching

typescript
const users = new DataSource<User>({
  url: '/api/users',
  cache: {
    enabled: true,
    ttl: 300000,     // 5 minutes (in milliseconds)
    maxSize: 1000    // Max 1000 cached items
  }
});

// First call: Makes HTTP request
await users.findById(1);

// Second call within 5 minutes: Returns from cache
await users.findById(1);  // No HTTP request

// After 5 minutes: Cache expired, makes new request
await users.findById(1);

Cache Management

typescript
// Clear all cache
users.clearCache();

// Invalidate specific cache key
users.invalidateCache('1');

// Cache is automatically invalidated on:
// - create()
// - update()
// - delete()
// - Any batch operation

Cache Configuration Options

typescript
interface CacheConfig {
  enabled: boolean;     // Enable/disable caching
  ttl?: number;         // Time to live in ms (default: no expiration)
  maxSize?: number;     // Max cached items (default: unlimited)
}

// Examples

// No expiration
cache: {
  enabled: true
}

// 10 minute TTL
cache: {
  enabled: true,
  ttl: 600000
}

// Limited size
cache: {
  enabled: true,
  ttl: 300000,
  maxSize: 500
}

// Disable caching
cache: {
  enabled: false
}

Best Practices

typescript
// ✅ Good - Cache read-heavy data
const products = new DataSource<Product>({
  url: '/api/products',
  cache: {
    enabled: true,
    ttl: 600000  // 10 minutes - products don't change often
  }
});

// ✅ Good - Short TTL for frequently updated data
const notifications = new DataSource<Notification>({
  url: '/api/notifications',
  cache: {
    enabled: true,
    ttl: 30000  // 30 seconds
  }
});

// ❌ Bad - Don't cache rapidly changing data
const liveScores = new DataSource<Score>({
  url: '/api/live-scores',
  cache: {
    enabled: true,
    ttl: 300000  // Too long for live data
  }
});
// Use real-time updates instead (see Real-time Configuration)

Real-time Configuration

Enable WebSocket-based real-time updates.

Basic Real-time Setup

typescript
const messages = new DataSource<Message>({
  url: '/api/messages',
  realtime: {
    enabled: true,
    url: 'wss://api.example.com/ws',
    reconnect: true,
    reconnectInterval: 5000
  }
});

// WebSocket connects automatically
// Listen for real-time events
messages.on('realtime:message', (data) => {
  console.log('New message:', data);
});

// Connection events
messages.on('ws:connected', () => {
  console.log('WebSocket connected');
});

messages.on('ws:disconnected', () => {
  console.log('WebSocket disconnected');
});

messages.on('ws:error', (error) => {
  console.error('WebSocket error:', error);
});

Real-time Configuration Options

typescript
interface RealtimeConfig {
  enabled: boolean;           // Enable/disable real-time
  url?: string;               // WebSocket server URL
  reconnect?: boolean;        // Auto-reconnect on disconnect
  reconnectInterval?: number; // Time between reconnect attempts (ms)
}

// Examples

// Basic
realtime: {
  enabled: true,
  url: 'wss://api.example.com/ws'
}

// With auto-reconnect
realtime: {
  enabled: true,
  url: 'wss://api.example.com/ws',
  reconnect: true,
  reconnectInterval: 5000  // Try reconnecting every 5 seconds
}

// Disable
realtime: {
  enabled: false
}

Manual WebSocket Control

typescript
// Connect manually
messages.connectWebSocket();

// Send custom message
messages.sendWebSocketMessage({
  type: 'subscribe',
  channel: 'chat-room-1'
});

// Disconnect
messages.disconnectWebSocket();

Real-time Message Handling

Expected WebSocket message format:

json
{
  "type": "message.created",
  "data": {
    "id": 123,
    "text": "Hello!",
    "userId": 456
  }
}

Handle in your code:

typescript
messages.on('realtime:message', (payload) => {
  const { type, data } = payload;
  
  switch (type) {
    case 'message.created':
      // Add new message to UI
      updateUI(data);
      break;
    
    case 'message.updated':
      // Update existing message
      updateMessage(data);
      break;
    
    case 'message.deleted':
      // Remove message from UI
      removeMessage(data.id);
      break;
  }
});

Retry Configuration

Configure automatic retry for failed requests.

Basic Retry

typescript
const users = new DataSource<User>({
  url: '/api/users',
  retry: {
    maxRetries: 3,
    retryDelay: 1000,
    backoffMultiplier: 2
  }
});

// If request fails, automatically retries with exponential backoff:
// Attempt 1: immediate
// Attempt 2: after 1000ms
// Attempt 3: after 2000ms
// Attempt 4: after 4000ms

Retry Configuration Options

typescript
interface RetryConfig {
  maxRetries?: number;        // Max retry attempts (default: 3)
  retryDelay?: number;        // Initial delay in ms (default: 1000)
  backoffMultiplier?: number; // Backoff multiplier (default: 2)
}

// Examples

// Default retry
retry: {
  maxRetries: 3,
  retryDelay: 1000,
  backoffMultiplier: 2
}

// Aggressive retry (for critical operations)
retry: {
  maxRetries: 5,
  retryDelay: 500,
  backoffMultiplier: 1.5
}

// Conservative retry (for expensive operations)
retry: {
  maxRetries: 2,
  retryDelay: 2000,
  backoffMultiplier: 3
}

// Disable retry
retry: {
  maxRetries: 0
}

When Retry Happens

Retry is triggered for:

  • ✅ Network errors (timeout, connection failed)
  • ✅ 5xx server errors (500, 502, 503, 504)
  • ❌ 4xx client errors (400, 401, 403, 404) - No retry

Security Configuration

Configure CSRF protection and input sanitization.

CSRF Token

typescript
const users = new DataSource<User>({
  url: '/api/users',
  security: {
    csrfToken: document.querySelector('meta[name="csrf"]')?.content,
    csrfHeader: 'X-CSRF-Token'
  }
});

// All write operations (POST, PUT, DELETE) will include:
// Headers: { 'X-CSRF-Token': 'token_value' }

Input Sanitization

Automatically remove HTML tags and scripts from specified fields.

typescript
const posts = new DataSource<Post>({
  url: '/api/posts',
  security: {
    sanitizeFields: ['title', 'content', 'bio']
  }
});

// Before sanitization:
await posts.create({
  title: 'My Post',
  content: '<script>alert("xss")</script>Hello'
});

// After sanitization (sent to API):
{
  title: 'My Post',
  content: 'Hello'  // <script> tag removed
}

Security Configuration Options

typescript
interface SecurityConfig {
  csrfToken?: string;           // CSRF token value
  csrfHeader?: string;          // Header name for CSRF token
  sanitizeFields?: (keyof T)[]; // Fields to sanitize
}

// Complete example
security: {
  csrfToken: getCsrfToken(),
  csrfHeader: 'X-CSRF-Token',
  sanitizeFields: ['title', 'content', 'bio', 'comments']
}

Validation Configuration

Define validation rules for your data.

Basic Validation

typescript
import type { FieldConfig } from 'bindra';

const fields: FieldConfig<User>[] = [
  {
    name: 'email',
    label: 'Email Address',
    validation: {
      required: true,
      type: 'email',
      message: 'Please enter a valid email'
    }
  },
  {
    name: 'age',
    label: 'Age',
    validation: {
      required: true,
      type: 'number',
      min: 18,
      max: 120,
      message: 'Age must be between 18 and 120'
    }
  }
];

const users = new DataSource<User>({
  url: '/api/users',
  fields
});

// Validation runs automatically on create/update
try {
  await users.create({
    email: 'invalid-email',
    age: 15
  });
} catch (error) {
  console.log(error.details);
  // {
  //   email: 'Please enter a valid email',
  //   age: 'Age must be between 18 and 120'
  // }
}

Validation Rules

typescript
interface FieldValidation<T> {
  required?: boolean;
  type?: 'string' | 'number' | 'boolean' | 'date' | 'email' | 'url' | 'array' | 'object';
  min?: number;    // Min value (number) or length (string/array)
  max?: number;    // Max value (number) or length (string/array)
  pattern?: RegExp;
  custom?: (value: any, record: Partial<T>) => boolean | string | Promise<boolean | string>;
  message?: string; // Custom error message
}

Complete Field Configuration

typescript
const fields: FieldConfig<User>[] = [
  // Required string with length validation
  {
    name: 'username',
    label: 'Username',
    validation: {
      required: true,
      type: 'string',
      min: 3,
      max: 20,
      pattern: /^[a-zA-Z0-9_]+$/,
      message: 'Username must be 3-20 alphanumeric characters'
    }
  },
  
  // Email validation
  {
    name: 'email',
    validation: {
      required: true,
      type: 'email'
    }
  },
  
  // Number range
  {
    name: 'age',
    validation: {
      type: 'number',
      min: 0,
      max: 150
    }
  },
  
  // Custom async validation
  {
    name: 'username',
    validation: {
      custom: async (value) => {
        const available = await checkUsernameAvailable(value);
        return available || 'Username already taken';
      }
    }
  },
  
  // Default value
  {
    name: 'status',
    defaultValue: 'active'
  },
  
  // Computed field
  {
    name: 'fullName',
    computed: (user) => `${user.firstName} ${user.lastName}`,
    readOnly: true
  },
  
  // Transform before validation
  {
    name: 'email',
    transform: (value) => value.toLowerCase().trim(),
    validation: {
      type: 'email'
    }
  }
];

Complete Examples

typescript
import { DataSource } from 'bindra';
import type { DataSourceConfig, FieldConfig } from 'bindra';

interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
  category: string;
  inStock: boolean;
  imageUrl: string;
  tags: string[];
  createdAt: Date;
  updatedAt: Date;
}

const fields: FieldConfig<Product>[] = [
  {
    name: 'name',
    label: 'Product Name',
    validation: {
      required: true,
      type: 'string',
      min: 3,
      max: 100
    }
  },
  {
    name: 'description',
    label: 'Description',
    validation: {
      type: 'string',
      max: 1000
    }
  },
  {
    name: 'price',
    label: 'Price',
    validation: {
      required: true,
      type: 'number',
      min: 0,
      custom: (value) => {
        // Must be valid currency (2 decimals max)
        return /^\d+(\.\d{1,2})?$/.test(value.toString()) || 'Invalid price format';
      }
    }
  },
  {
    name: 'category',
    label: 'Category',
    validation: {
      required: true,
      custom: (value) => {
        const validCategories = ['electronics', 'clothing', 'food', 'books', 'toys'];
        return validCategories.includes(value) || 'Invalid category';
      }
    }
  },
  {
    name: 'inStock',
    defaultValue: true
  },
  {
    name: 'imageUrl',
    validation: {
      type: 'url'
    }
  },
  {
    name: 'tags',
    validation: {
      type: 'array',
      max: 10
    },
    defaultValue: []
  }
];

const config: DataSourceConfig<Product> = {
  // API endpoint
  url: 'https://api.example.com/products',
  
  // Field validation
  fields,
  
  // Pagination
  pageSize: 24,
  
  // Caching (products don't change frequently)
  cache: {
    enabled: true,
    ttl: 600000,    // 10 minutes
    maxSize: 1000
  },
  
  // Retry configuration
  retry: {
    maxRetries: 3,
    retryDelay: 1000,
    backoffMultiplier: 2
  },
  
  // Security
  security: {
    csrfToken: document.querySelector('meta[name="csrf"]')?.content,
    csrfHeader: 'X-CSRF-Token',
    sanitizeFields: ['name', 'description']
  }
};

const products = new DataSource<Product>(config);

// Usage
await products.fetch({
  filter: { category: 'electronics', inStock: true },
  sort: 'price',
  limit: 24
});

// Create with validation
const newProduct = await products.create({
  name: 'Wireless Mouse',
  description: 'Ergonomic wireless mouse',
  price: 29.99,
  category: 'electronics',
  imageUrl: 'https://example.com/mouse.jpg',
  tags: ['wireless', 'ergonomic']
});

Real-time Chat Application

typescript
interface Message {
  id: number;
  text: string;
  userId: number;
  roomId: number;
  timestamp: Date;
}

const fields: FieldConfig<Message>[] = [
  {
    name: 'text',
    validation: {
      required: true,
      type: 'string',
      min: 1,
      max: 500
    }
  },
  {
    name: 'roomId',
    validation: {
      required: true,
      type: 'number'
    }
  }
];

const messages = new DataSource<Message>({
  url: 'https://api.example.com/messages',
  fields,
  
  // Small page size for chat
  pageSize: 50,
  
  // Real-time updates
  realtime: {
    enabled: true,
    url: 'wss://api.example.com/ws',
    reconnect: true,
    reconnectInterval: 3000
  },
  
  // No caching for real-time data
  cache: {
    enabled: false
  },
  
  // Security
  security: {
    csrfToken: getCsrfToken(),
    csrfHeader: 'X-CSRF-Token',
    sanitizeFields: ['text']  // Prevent XSS
  }
});

// Listen for new messages
messages.on('realtime:message', (payload) => {
  if (payload.type === 'message.created') {
    displayMessage(payload.data);
  }
});

// Send message
await messages.create({
  text: 'Hello, world!',
  roomId: 1
});

Offline-First Todo App

typescript
interface Todo {
  id: number;
  title: string;
  completed: boolean;
  createdAt: Date;
}

// Start with local data
const todos = new DataSource<Todo>({
  data: JSON.parse(localStorage.getItem('todos') || '[]'),
  fields: [
    {
      name: 'title',
      validation: {
        required: true,
        type: 'string',
        min: 1,
        max: 200
      }
    },
    {
      name: 'completed',
      defaultValue: false
    }
  ]
});

// Save to localStorage on changes
todos.on('dataChanged', (data) => {
  localStorage.setItem('todos', JSON.stringify(data));
});

// Operations work entirely offline
await todos.create({ title: 'Learn Bindra' });
await todos.update(1, { completed: true });
await todos.delete(2);

Configuration Cheat Sheet

typescript
// Minimal
{ url: '/api/users' }

// Read-heavy app
{
  url: '/api/users',
  cache: { enabled: true, ttl: 600000 },
  pageSize: 50
}

// Real-time app
{
  url: '/api/messages',
  realtime: {
    enabled: true,
    url: 'wss://api.example.com/ws',
    reconnect: true
  },
  cache: { enabled: false }
}

// Form with validation
{
  url: '/api/users',
  fields: [
    { name: 'email', validation: { required: true, type: 'email' } },
    { name: 'age', validation: { type: 'number', min: 18 } }
  ]
}

// Secure app
{
  url: '/api/posts',
  security: {
    csrfToken: getCsrfToken(),
    csrfHeader: 'X-CSRF-Token',
    sanitizeFields: ['title', 'content']
  }
}

// Unreliable network
{
  url: '/api/data',
  retry: {
    maxRetries: 5,
    retryDelay: 2000,
    backoffMultiplier: 2
  },
  cache: { enabled: true, ttl: 300000 }
}

// Offline-first
{
  data: getLocalData()
}


← Back to API Reference

Released under the MIT License.