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:
- Storing collection records
- Fetching records by ID
- Querying records with filters
- 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:
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
// 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
// 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
// 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
# .env.local
BLOCKSWEB_API_URL=https://api.blocksweb.com
BLOCKSWEB_PROJECT_ID=your-project-id
BLOCKSWEB_API_KEY=your-api-key # Optional, for private projectsFeatures
✅ 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
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
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
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
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
// 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
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
export const collectionProvider = new RestAPICollectionProvider({
baseUrl: process.env.API_URL!,
apiKey: process.env.API_KEY!
});Provider Selection Strategy
Development → Production
// 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:
// 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
// ✅ 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
// ✅ 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
// 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
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
| Feature | MemoryCollectionProvider | CMSCollectionProvider | Custom Provider |
|---|---|---|---|
| Setup Complexity | ⭐ Simple | ⭐⭐ Moderate | ⭐⭐⭐ Complex |
| Persistence | ❌ No | ✅ Yes | Depends |
| Performance | ⭐⭐⭐ Fast | ⭐⭐ Good | Depends |
| Multi-user | ❌ No | ✅ Yes | Depends |
| Asset Management | ❌ No | ✅ Yes | Depends |
| Best For | Development | Production | Integration |
| Cost | Free | Paid | Depends |
Next Steps
- Defining Collections - Create collection structures
- Collections Overview - Understand the system
Troubleshooting
Provider Not Found Error
// ❌ Error: collectionProvider is undefined
// Cause: Not exported from blocksweb.config.ts
// ✅ Fix: Export the provider
export const collectionProvider = new CMSCollectionProvider({...});API Connection Failed
// ❌ 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
// ❌ 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
});