@bernierllc/contentful-cda-client
v1.0.4
Published
Content Delivery API (CDA) client wrapper with Sync API support for published content retrieval
Readme
@bernierllc/contentful-cda-client
Content Delivery API (CDA) client wrapper for published content retrieval and sync operations.
Overview
Wrapper around the official contentful npm package for Content Delivery API access. Provides published content retrieval, Sync API for incremental updates, and CDN-backed queries with high rate limits.
Features
- ✅ Published Content Access - Fetch entries and assets from CDN
- ✅ Sync API Support - Initial sync + delta sync for incremental updates
- ✅ Locale-Aware - Fetch content in specific languages
- ✅ Include Depth - Control reference resolution depth
- ✅ High Rate Limits - CDN-backed queries (55 requests/sec)
- ✅ Type-Safe - Full TypeScript support with strict typing
- ✅ Logger Integration - Built-in structured logging
What This Package Does NOT Do (MECE)
- ❌ Draft/unpublished content (use
@bernierllc/contentful-cma-client) - ❌ Content modification (use
@bernierllc/contentful-cma-client) - ❌ GraphQL queries (use
@bernierllc/contentful-graphql-client)
Installation
npm install @bernierllc/contentful-cda-clientQuick Start
import { ContentfulCDAClient } from '@bernierllc/contentful-cda-client';
// Initialize client
const client = new ContentfulCDAClient({
space: 'your-space-id',
accessToken: 'your-cda-token', // CDA token (not CMA token!)
environment: 'master'
});
// Fetch single entry
const entry = await client.getEntry('entryId');
// Fetch entries with query
const posts = await client.getEntries({
content_type: 'blogPost',
limit: 10,
order: '-sys.createdAt'
});
// Initial sync
const syncResult = await client.initialSync();
console.log(`Synced ${syncResult.entries.length} entries`);
// Delta sync (incremental updates)
const deltaResult = await client.deltaSync(syncResult.nextSyncToken);
console.log(`Updated ${deltaResult.entries.length} entries`);API Reference
Constructor
new ContentfulCDAClient(config)
Creates a new CDA client instance.
interface ContentfulCDAConfig {
space: string; // Required: Contentful space ID
accessToken: string; // Required: CDA access token
environment?: string; // Optional: Environment (default: 'master')
host?: string; // Optional: API host (default: 'cdn.contentful.com')
retryOnError?: boolean; // Optional: Retry on error (default: true)
timeout?: number; // Optional: Request timeout (default: 30000ms)
}Example:
const client = new ContentfulCDAClient({
space: 'abc123xyz',
accessToken: 'your-cda-token',
environment: 'master',
timeout: 60000
});Entry Methods
getEntry<T>(entryId, locale?)
Retrieves a single published entry by ID.
interface BlogPost {
title: string;
body: string;
author: ContentfulLink<'Entry'>;
}
const post = await client.getEntry<BlogPost>('5KsDBWseXY6QegucYAoacS');
console.log(post.fields.title);
// Fetch in specific locale
const spanishPost = await client.getEntry<BlogPost>('5KsDBWseXY6QegucYAoacS', 'es-ES');getEntries<T>(query?)
Retrieves multiple published entries based on query parameters.
// Fetch all blog posts
const posts = await client.getEntries({
content_type: 'blogPost'
});
// Fetch with pagination
const entries = await client.getEntries({
content_type: 'article',
limit: 10,
skip: 20,
order: '-sys.createdAt',
include: 2 // Include depth for references
});
// Fetch in specific locale
const frenchArticles = await client.getEntries({
content_type: 'article',
locale: 'fr-FR'
});
// Filter by field values
const featuredPosts = await client.getEntries({
content_type: 'blogPost',
'fields.featured': true,
limit: 5
});Query Parameters:
content_type: Filter by content type IDlimit: Number of items to return (default: 100, max: 1000)skip: Number of items to skip (for pagination)order: Sort order (e.g.,sys.createdAt,-sys.updatedAt)include: Include depth for linked entries/assets (0-10)locale: Fetch content in specific localefields.[fieldName]: Filter by field valuesselect: Select specific fields to return
Asset Methods
getAsset(assetId, locale?)
Retrieves a single published asset by ID.
const asset = await client.getAsset('1MvhB4KOyyCMIo0KeeqUaa');
console.log(asset.fields.file['en-US'].url); // Image URL
console.log(asset.fields.file['en-US'].details.size); // File sizegetAssets(query?)
Retrieves multiple published assets based on query parameters.
// Fetch all images
const images = await client.getAssets({
mimetype_group: 'image',
limit: 20
});
// Fetch with pagination and sorting
const assets = await client.getAssets({
limit: 50,
skip: 0,
order: '-sys.createdAt'
});Sync API Methods
initialSync(options?)
Performs an initial sync to retrieve all published content. Returns a sync token for subsequent delta syncs.
// Sync all content
const syncResult = await client.initialSync();
console.log(`Synced ${syncResult.entries.length} entries`);
console.log(`Synced ${syncResult.assets.length} assets`);
// Store the sync token
const syncToken = syncResult.nextSyncToken;
// Sync only entries (not assets)
const entrySync = await client.initialSync({ type: 'Entry' });
// Sync specific content type
const blogSync = await client.initialSync({
type: 'Entry',
content_type: 'blogPost'
});
// Sync in specific locale
const frenchSync = await client.initialSync({ locale: 'fr-FR' });Options:
interface ContentfulSyncOptions {
type?: 'all' | 'Asset' | 'Entry' | 'Deletion' | 'DeletedAsset' | 'DeletedEntry';
content_type?: string; // Filter by content type
locale?: string; // Filter by locale
limit?: number; // Items per page
}deltaSync(nextSyncToken)
Performs a delta sync using a sync token from a previous sync. Returns only changes since the last sync.
// Perform delta sync
const deltaResult = await client.deltaSync(storedSyncToken);
// Process new/updated content
deltaResult.entries.forEach(entry => {
console.log(`Updated entry: ${entry.sys.id}`);
});
deltaResult.assets.forEach(asset => {
console.log(`Updated asset: ${asset.sys.id}`);
});
// Process deletions
deltaResult.deletedEntries.forEach(deleted => {
console.log(`Deleted entry: ${deleted.sys.id}`);
});
deltaResult.deletedAssets.forEach(deleted => {
console.log(`Deleted asset: ${deleted.sys.id}`);
});
// Store new token for next sync
storedSyncToken = deltaResult.nextSyncToken;Sync Collection Response:
interface SyncCollection {
entries: Entry[]; // New/updated entries
assets: Asset[]; // New/updated assets
deletedEntries: Entry[]; // Deleted entries
deletedAssets: Asset[]; // Deleted assets
nextSyncToken: string; // Token for next delta sync
nextPageUrl?: string; // URL for pagination (if needed)
}Utility Methods
getConfig()
Returns the current client configuration (without exposing the access token).
const config = client.getConfig();
console.log(config.space); // 'your-space-id'
console.log(config.environment); // 'master'
// config.accessToken is NOT included for securitygetRawClient()
Returns the underlying Contentful SDK client for advanced operations.
const rawClient = client.getRawClient();
// Use for operations not covered by the wrapperUsage Examples
Building a Content Cache
import { ContentfulCDAClient } from '@bernierllc/contentful-cda-client';
class ContentCache {
private client: ContentfulCDAClient;
private syncToken: string | null = null;
private cache: Map<string, any> = new Map();
constructor(config: ContentfulCDAConfig) {
this.client = new ContentfulCDAClient(config);
}
async initialize(): Promise<void> {
console.log('Initializing content cache...');
const syncResult = await this.client.initialSync();
// Store all entries in cache
syncResult.entries.forEach(entry => {
this.cache.set(entry.sys.id, entry);
});
// Store sync token for updates
this.syncToken = syncResult.nextSyncToken;
console.log(`Cache initialized with ${syncResult.entries.length} entries`);
}
async update(): Promise<void> {
if (!this.syncToken) {
throw new Error('Cache not initialized');
}
console.log('Updating cache...');
const deltaResult = await this.client.deltaSync(this.syncToken);
// Update cache with changes
deltaResult.entries.forEach(entry => {
this.cache.set(entry.sys.id, entry);
});
// Remove deleted entries
deltaResult.deletedEntries.forEach(deleted => {
this.cache.delete(deleted.sys.id);
});
// Update sync token
this.syncToken = deltaResult.nextSyncToken;
console.log(`Cache updated: +${deltaResult.entries.length} entries, -${deltaResult.deletedEntries.length} deleted`);
}
get(id: string): any | undefined {
return this.cache.get(id);
}
}
// Usage
const cache = new ContentCache({
space: 'your-space-id',
accessToken: 'your-cda-token'
});
await cache.initialize();
// Update cache every 5 minutes
setInterval(() => cache.update(), 5 * 60 * 1000);Fetching Localized Content
async function getLocalizedContent(contentType: string, locale: string) {
const client = new ContentfulCDAClient({
space: 'your-space-id',
accessToken: 'your-cda-token'
});
const entries = await client.getEntries({
content_type: contentType,
locale: locale
});
return entries;
}
// Fetch blog posts in Spanish
const spanishPosts = await getLocalizedContent('blogPost', 'es-ES');
// Fetch products in German
const germanProducts = await getLocalizedContent('product', 'de-DE');Paginated Content Fetching
async function fetchAllEntries(contentType: string) {
const client = new ContentfulCDAClient({
space: 'your-space-id',
accessToken: 'your-cda-token'
});
let allEntries: any[] = [];
let skip = 0;
const limit = 100;
let hasMore = true;
while (hasMore) {
const entries = await client.getEntries({
content_type: contentType,
limit: limit,
skip: skip
});
allEntries = allEntries.concat(entries);
skip += limit;
hasMore = entries.length === limit;
}
return allEntries;
}
// Fetch all blog posts (handles pagination automatically)
const allPosts = await fetchAllEntries('blogPost');
console.log(`Fetched ${allPosts.length} total posts`);Rate Limits
The Content Delivery API is served via CDN with high rate limits:
- CDA: 55 requests/second (CDN-backed)
- Preview API: 14 requests/second
For most use cases, CDA rate limits are generous. If you need higher throughput:
- Use the Sync API for bulk operations
- Implement client-side caching
- Use
includeparameter to fetch linked content in single request
Error Handling
try {
const entry = await client.getEntry('invalid-id');
} catch (error) {
if (error.sys?.id === 'NotFound') {
console.error('Entry not found');
} else if (error.sys?.id === 'RateLimitExceeded') {
console.error('Rate limit exceeded, retry after:', error.sys.retryAfter);
} else {
console.error('Unknown error:', error);
}
}Testing
The package includes comprehensive test coverage (90%+):
npm test # Run tests
npm run test:coverage # Run with coverage report
npm run test:watch # Watch modeDependencies
@bernierllc/contentful-types- Shared Contentful type definitions@bernierllc/contentful-client-core- Shared HTTP client utilities@bernierllc/logger- Structured loggingcontentful- Official Contentful SDK
Related Packages
Core Layer
@bernierllc/contentful-types- Shared type definitions@bernierllc/contentful-client-core- HTTP client core@bernierllc/contentful-cma-client- Management API client (draft content, modifications)@bernierllc/contentful-graphql-client- GraphQL API client
Service Layer
@bernierllc/contentful-sync-service- Automated sync orchestration@bernierllc/contentful-cache-service- Content caching service
License
Copyright (c) 2025 Bernier LLC. See LICENSE for details.
Support
For issues and questions, contact Bernier LLC support or file an issue in the repository.
