Skip to content

Collection Providers

Collection providers are the data source layer for BlocksWeb collections. They determine where collection data comes from and how it's fetched.

Registration Required

Providers must be registered with collections in blocksweb.config.ts. See the BlocksWeb Configuration guide for registration instructions.

What Are Collection Providers?

A collection provider is responsible for:

  1. Storing collection records
  2. Fetching records by ID
  3. Querying records with filters
  4. Resolving IDs to full objects

BlocksWeb abstracts the data source behind this interface, so you can:

  • Use in-memory data for development
  • Connect to BlocksWeb CMS for production
  • Build custom integrations with your existing database
  • Switch providers without changing component code

The Provider Interface

All providers implement the CollectionProvider interface:

typescript
interface CollectionProvider {
  // Fetch a single record by ID
  getRecord(collectionName: string, id: string): Promise<any>;

  // Fetch multiple records by IDs
  getRecords(collectionName: string, ids: string[]): Promise<any[]>;

  // Query records with filters, sorting, pagination
  queryRecords(
    collectionName: string,
    query?: CollectionQuery
  ): Promise<{ records: any[]; total: number }>;

  // Optional: Create a new record
  createRecord?(collectionName: string, data: any): Promise<any>;

  // Optional: Update an existing record
  updateRecord?(collectionName: string, id: string, data: any): Promise<any>;

  // Optional: Delete a record
  deleteRecord?(collectionName: string, id: string): Promise<void>;
}

Built-in Providers

BlocksWeb includes two production-ready providers:

1. MemoryCollectionProvider

Best for: Local development, testing, prototyping

Stores data in memory - no database or API required.

Basic Setup

typescript
// blocksweb.config.ts
import { MemoryCollectionProvider } from '@blocksweb/core';

export const collectionProvider = new MemoryCollectionProvider({
  data: {
    products: [
      {
        id: '1',
        name: 'Awesome Product',
        price: 99.99,
        description: 'The best product ever',
        inStock: true
      },
      {
        id: '2',
        name: 'Another Product',
        price: 149.99,
        description: 'Also great',
        inStock: false
      }
    ],
    blog: [
      {
        id: '1',
        title: 'Getting Started with BlocksWeb',
        slug: 'getting-started',
        content: '<p>Welcome to BlocksWeb...</p>',
        publishedAt: '2024-01-15',
        featured: true
      }
    ]
  }
});

Features

Instant setup - No configuration needed ✅ Fast - All data in memory ✅ Works offline - No API calls ✅ Perfect for demos - Quick prototyping

Data not persisted - Lost on restart ❌ Not for production - No real database ❌ Single instance - No collaboration

Use Cases

typescript
// Development workflow
if (process.env.NODE_ENV === 'development') {
  collectionProvider = new MemoryCollectionProvider({
    data: mockData
  });
} else {
  collectionProvider = new CMSCollectionProvider({
    apiUrl: process.env.BLOCKSWEB_API_URL
  });
}
  • Local development - Fast iteration without API
  • Testing - Predictable test data
  • Prototyping - Build UI before backend
  • Demos - Show functionality without setup

2. CMSCollectionProvider

Best for: Production applications with BlocksWeb CMS

Fetches data from the BlocksWeb CMS API - the recommended production provider.

Basic Setup

typescript
// blocksweb.config.ts
import { CMSCollectionProvider } from '@blocksweb/core';

export const collectionProvider = new CMSCollectionProvider({
  apiUrl: process.env.BLOCKSWEB_API_URL!,
  projectId: process.env.BLOCKSWEB_PROJECT_ID!,
  apiKey: process.env.BLOCKSWEB_API_KEY
});

Environment Variables

bash
# .env.local
BLOCKSWEB_API_URL=https://api.blocksweb.com
BLOCKSWEB_PROJECT_ID=your-project-id
BLOCKSWEB_API_KEY=your-api-key  # Optional, for private projects

Features

Full CMS integration - Content editors manage data ✅ Real-time updates - Changes reflect immediately ✅ Multi-user - Team collaboration ✅ Asset management - Images and files ✅ Versioning - Track changes ✅ Multi-language - i18n support ✅ Production-ready - Scalable and reliable

Configuration Options

typescript
new CMSCollectionProvider({
  // Required
  apiUrl: string;           // BlocksWeb API URL
  projectId: string;        // Your project ID

  // Optional
  apiKey?: string;          // For private projects
  locale?: string;          // Default locale (e.g., 'en', 'nl')
  cache?: boolean;          // Enable caching (default: true)
  cacheTime?: number;       // Cache TTL in ms (default: 60000)
  retryAttempts?: number;   // Retry failed requests (default: 3)
});

With Caching

typescript
export const collectionProvider = new CMSCollectionProvider({
  apiUrl: process.env.BLOCKSWEB_API_URL!,
  projectId: process.env.BLOCKSWEB_PROJECT_ID!,
  cache: true,
  cacheTime: 5 * 60 * 1000  // 5 minutes
});

Multi-Language Setup

typescript
export const collectionProvider = new CMSCollectionProvider({
  apiUrl: process.env.BLOCKSWEB_API_URL!,
  projectId: process.env.BLOCKSWEB_PROJECT_ID!,
  locale: 'en',  // Default locale
});

// Fetch in different locale
const dutchProducts = await collectionProvider.queryRecords('products', {
  locale: 'nl'
});

Custom Providers

Create custom providers to integrate with any data source.

Basic Custom Provider

typescript
import { CollectionProvider, CollectionQuery } from '@blocksweb/core';

export class DatabaseCollectionProvider implements CollectionProvider {
  private db: Database;

  constructor(dbConnection: Database) {
    this.db = dbConnection;
  }

  async getRecord(collectionName: string, id: string) {
    const result = await this.db.query(
      'SELECT * FROM ?? WHERE id = ?',
      [collectionName, id]
    );
    return result[0];
  }

  async getRecords(collectionName: string, ids: string[]) {
    if (ids.length === 0) return [];

    const result = await this.db.query(
      'SELECT * FROM ?? WHERE id IN (?)',
      [collectionName, ids]
    );
    return result;
  }

  async queryRecords(collectionName: string, query?: CollectionQuery) {
    const { filter, sort, limit, skip } = query || {};

    let sql = `SELECT * FROM ??`;
    const params: any[] = [collectionName];

    // Add filters
    if (filter) {
      const conditions = Object.entries(filter)
        .map(([key, value]) => `${key} = ?`)
        .join(' AND ');
      sql += ` WHERE ${conditions}`;
      params.push(...Object.values(filter));
    }

    // Add sorting
    if (sort) {
      const sortBy = Object.entries(sort)
        .map(([key, order]) => `${key} ${order === 1 ? 'ASC' : 'DESC'}`)
        .join(', ');
      sql += ` ORDER BY ${sortBy}`;
    }

    // Add pagination
    if (limit) {
      sql += ` LIMIT ?`;
      params.push(limit);
    }
    if (skip) {
      sql += ` OFFSET ?`;
      params.push(skip);
    }

    const records = await this.db.query(sql, params);

    // Get total count
    const totalResult = await this.db.query(
      'SELECT COUNT(*) as count FROM ??',
      [collectionName]
    );

    return {
      records,
      total: totalResult[0].count
    };
  }
}

Usage

typescript
// blocksweb.config.ts
import { DatabaseCollectionProvider } from './providers/database';
import { createConnection } from './db';

const db = createConnection({
  host: process.env.DB_HOST,
  database: process.env.DB_NAME
});

export const collectionProvider = new DatabaseCollectionProvider(db);

Advanced: REST API Provider

typescript
export class RestAPICollectionProvider implements CollectionProvider {
  private baseUrl: string;
  private headers: Record<string, string>;

  constructor(config: { baseUrl: string; apiKey: string }) {
    this.baseUrl = config.baseUrl;
    this.headers = {
      'Authorization': `Bearer ${config.apiKey}`,
      'Content-Type': 'application/json'
    };
  }

  async getRecord(collectionName: string, id: string) {
    const response = await fetch(
      `${this.baseUrl}/collections/${collectionName}/records/${id}`,
      { headers: this.headers }
    );

    if (!response.ok) {
      throw new Error(`Failed to fetch record: ${response.statusText}`);
    }

    return response.json();
  }

  async getRecords(collectionName: string, ids: string[]) {
    const response = await fetch(
      `${this.baseUrl}/collections/${collectionName}/records?ids=${ids.join(',')}`,
      { headers: this.headers }
    );

    if (!response.ok) {
      throw new Error(`Failed to fetch records: ${response.statusText}`);
    }

    return response.json();
  }

  async queryRecords(collectionName: string, query?: CollectionQuery) {
    const params = new URLSearchParams();

    if (query?.filter) {
      params.append('filter', JSON.stringify(query.filter));
    }
    if (query?.sort) {
      params.append('sort', JSON.stringify(query.sort));
    }
    if (query?.limit) {
      params.append('limit', query.limit.toString());
    }
    if (query?.skip) {
      params.append('skip', query.skip.toString());
    }

    const response = await fetch(
      `${this.baseUrl}/collections/${collectionName}/query?${params}`,
      { headers: this.headers }
    );

    if (!response.ok) {
      throw new Error(`Failed to query records: ${response.statusText}`);
    }

    return response.json();
  }

  async createRecord(collectionName: string, data: any) {
    const response = await fetch(
      `${this.baseUrl}/collections/${collectionName}/records`,
      {
        method: 'POST',
        headers: this.headers,
        body: JSON.stringify(data)
      }
    );

    if (!response.ok) {
      throw new Error(`Failed to create record: ${response.statusText}`);
    }

    return response.json();
  }

  async updateRecord(collectionName: string, id: string, data: any) {
    const response = await fetch(
      `${this.baseUrl}/collections/${collectionName}/records/${id}`,
      {
        method: 'PUT',
        headers: this.headers,
        body: JSON.stringify(data)
      }
    );

    if (!response.ok) {
      throw new Error(`Failed to update record: ${response.statusText}`);
    }

    return response.json();
  }

  async deleteRecord(collectionName: string, id: string) {
    const response = await fetch(
      `${this.baseUrl}/collections/${collectionName}/records/${id}`,
      {
        method: 'DELETE',
        headers: this.headers
      }
    );

    if (!response.ok) {
      throw new Error(`Failed to delete record: ${response.statusText}`);
    }
  }
}

Usage

typescript
export const collectionProvider = new RestAPICollectionProvider({
  baseUrl: process.env.API_URL!,
  apiKey: process.env.API_KEY!
});

Provider Selection Strategy

Development → Production

typescript
// blocksweb.config.ts
import { MemoryCollectionProvider, CMSCollectionProvider } from '@blocksweb/core';

const isDevelopment = process.env.NODE_ENV === 'development';

export const collectionProvider = isDevelopment
  ? new MemoryCollectionProvider({
      data: {
        products: [
          { id: '1', name: 'Product 1', price: 99 }
        ]
      }
    })
  : new CMSCollectionProvider({
      apiUrl: process.env.BLOCKSWEB_API_URL!,
      projectId: process.env.BLOCKSWEB_PROJECT_ID!
    });

Multiple Providers

Some projects need multiple providers:

typescript
// blocksweb.config.ts
import { CMSCollectionProvider } from '@blocksweb/core';
import { DatabaseCollectionProvider } from './providers/database';

// CMS for marketing content
const cmsProvider = new CMSCollectionProvider({
  apiUrl: process.env.BLOCKSWEB_API_URL!,
  projectId: process.env.BLOCKSWEB_PROJECT_ID!
});

// Database for product catalog
const dbProvider = new DatabaseCollectionProvider(db);

// Composite provider
export const collectionProvider = {
  async getRecord(collectionName: string, id: string) {
    // Use CMS for blog, team, testimonials
    if (['blog', 'team', 'testimonials'].includes(collectionName)) {
      return cmsProvider.getRecord(collectionName, id);
    }
    // Use database for products, orders
    return dbProvider.getRecord(collectionName, id);
  },

  async getRecords(collectionName: string, ids: string[]) {
    if (['blog', 'team', 'testimonials'].includes(collectionName)) {
      return cmsProvider.getRecords(collectionName, ids);
    }
    return dbProvider.getRecords(collectionName, ids);
  },

  async queryRecords(collectionName: string, query?: any) {
    if (['blog', 'team', 'testimonials'].includes(collectionName)) {
      return cmsProvider.queryRecords(collectionName, query);
    }
    return dbProvider.queryRecords(collectionName, query);
  }
};

Best Practices

1. Use Environment Variables

typescript
// ✅ Good - Configurable per environment
export const collectionProvider = new CMSCollectionProvider({
  apiUrl: process.env.BLOCKSWEB_API_URL!,
  projectId: process.env.BLOCKSWEB_PROJECT_ID!
});

// ❌ Bad - Hardcoded values
export const collectionProvider = new CMSCollectionProvider({
  apiUrl: 'https://api.blocksweb.com',
  projectId: 'my-project-123'
});

2. Enable Caching in Production

typescript
// ✅ Good - Faster performance
export const collectionProvider = new CMSCollectionProvider({
  apiUrl: process.env.BLOCKSWEB_API_URL!,
  projectId: process.env.BLOCKSWEB_PROJECT_ID!,
  cache: true,
  cacheTime: 5 * 60 * 1000  // 5 minutes
});

3. Handle Errors Gracefully

typescript
// Custom provider with error handling
export class SafeDatabaseProvider implements CollectionProvider {
  async getRecord(collectionName: string, id: string) {
    try {
      const result = await this.db.query(/* ... */);
      return result[0];
    } catch (error) {
      console.error(`Failed to fetch ${collectionName}/${id}:`, error);
      return null;  // Graceful fallback
    }
  }

  // ... other methods with try/catch
}

4. Add Request Retry Logic

typescript
export class ResilientAPIProvider implements CollectionProvider {
  private async fetchWithRetry(url: string, options: any, retries = 3) {
    for (let i = 0; i < retries; i++) {
      try {
        const response = await fetch(url, options);
        if (response.ok) return response;
      } catch (error) {
        if (i === retries - 1) throw error;
        await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
      }
    }
  }

  async getRecord(collectionName: string, id: string) {
    const response = await this.fetchWithRetry(
      `${this.baseUrl}/collections/${collectionName}/records/${id}`,
      { headers: this.headers }
    );
    return response.json();
  }
}

Provider Comparison

FeatureMemoryCollectionProviderCMSCollectionProviderCustom Provider
Setup Complexity⭐ Simple⭐⭐ Moderate⭐⭐⭐ Complex
Persistence❌ No✅ YesDepends
Performance⭐⭐⭐ Fast⭐⭐ GoodDepends
Multi-user❌ No✅ YesDepends
Asset Management❌ No✅ YesDepends
Best ForDevelopmentProductionIntegration
CostFreePaidDepends

Next Steps

Troubleshooting

Provider Not Found Error

typescript
// ❌ Error: collectionProvider is undefined
// Cause: Not exported from blocksweb.config.ts

// ✅ Fix: Export the provider
export const collectionProvider = new CMSCollectionProvider({...});

API Connection Failed

typescript
// ❌ Error: Failed to connect to API
// Cause: Wrong API URL or missing credentials

// ✅ Fix: Check environment variables
console.log('API URL:', process.env.BLOCKSWEB_API_URL);
console.log('Project ID:', process.env.BLOCKSWEB_PROJECT_ID);

Data Not Updating

typescript
// ❌ Issue: Changes not reflected
// Cause: Caching too aggressive

// ✅ Fix: Reduce cache time or disable during dev
export const collectionProvider = new CMSCollectionProvider({
  apiUrl: process.env.BLOCKSWEB_API_URL!,
  projectId: process.env.BLOCKSWEB_PROJECT_ID!,
  cache: process.env.NODE_ENV !== 'development',
  cacheTime: 1 * 60 * 1000  // 1 minute in dev
});