@se-studio/contentful-rest-api
v1.27.0
Published
Type-safe Contentful REST API client with caching and rate limiting for Next.js applications
Downloads
12,254
Maintainers
Readme
@se-studio/contentful-rest-api
Type-safe Contentful REST API client with caching, rate limiting, and extensible converters for Next.js applications.
Table of Contents
- Features
- Installation
- Quick Start
- API Reference
- Converter Pattern
- Caching
- Error Handling
- Retry Configuration
- Rate Limiting
- Type Generation
- Configuration Options
- Next.js App Router Integration
- License
- Repository
Features
- 🎯 Type-safe - Full TypeScript support with generated types from your Contentful space
- 🔄 Dual API Support - Content Delivery API (CDA) and Content Preview API (CPA)
- ⚡ Next.js Optimized - Built-in support for Next.js App Router cache tags and revalidation
- 🔁 Retry Logic - Automatic retry with exponential backoff for failed requests
- 🚦 Rate Limiting - Respect Contentful API rate limits with built-in rate limiter
- 🧩 Extensible Converters - Functional composition pattern for customizing content transformations
- 🖼️ Capped image origins - Raster picture
srcURLs use Contentful Images APIw/fm/qsonext/imagenever downloads camera-raw masters - 🛡️ Error Handling - Custom error types for different failure scenarios
Installation
pnpm add @se-studio/contentful-rest-api @se-studio/core-data-types contentfulQuick Start
Basic Usage
import { contentfulPageRest } from '@se-studio/contentful-rest-api';
// Fetch a page by slug
const page = await contentfulPageRest(
{
spaceId: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
environment: 'master'
},
'home',
{
locale: 'en-US',
cache: {
tags: ['home-page'],
revalidate: 3600 // 1 hour
}
}
);Preview Mode
import { contentfulPageRest } from '@se-studio/contentful-rest-api';
// Use preview API for draft content
const page = await contentfulPageRest(
{
spaceId: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN!,
},
'home',
{
preview: true, // Uses Preview API (CPA)
cache: {
cache: 'no-store' // Disable Next.js caching for preview content
}
}
);API Reference
Client Functions
createContentfulClient(config)
Creates a Contentful Content Delivery API (CDA) client.
import { createContentfulClient } from '@se-studio/contentful-rest-api';
const client = createContentfulClient({
spaceId: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
environment: 'master'
});createContentfulPreviewClient(config)
Creates a Contentful Content Preview API (CPA) client.
import { createContentfulPreviewClient } from '@se-studio/contentful-rest-api';
const previewClient = createContentfulPreviewClient({
spaceId: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN!,
});Content Fetching Functions
contentfulPageRest(config, slug, options?, converter?)
Fetches a page by slug.
Parameters:
config: ContentfulConfig - Contentful client configurationslug: string - Page slug to fetchoptions?: FetchOptions - Optional fetch options (locale, preview, cache, retry)converter?: Converter - Optional custom converter function
Returns: Promise<IPage | null>
const page = await contentfulPageRest(
config,
'about-us',
{
locale: 'en-US',
include: 10,
cache: { tags: ['about-page'], revalidate: 3600 }
}
);contentfulPageByIdRest(config, id, options?, converter?)
Fetches a page by entry ID.
Parameters:
config: ContentfulConfigid: string - Entry IDoptions?: FetchOptionsconverter?: Converter
Returns: Promise<IPage>
Throws: EntryNotFoundError if entry doesn't exist
const page = await contentfulPageByIdRest(
config,
'5nZHNlP9rZhWvKx4w2Z8zB'
);contentfulAllPagesRest(config, options?, converter?)
Fetches all pages from Contentful.
Parameters:
config: ContentfulConfigoptions?: FetchOptionsconverter?: Converter
Returns: Promise<IPage[]>
const pages = await contentfulAllPagesRest(
config,
{
locale: 'en-US',
cache: { tags: ['all-pages'], revalidate: 3600 }
}
);contentfulEntryRest<TEntry, TResult>(config, id, converter, options?)
Generic function to fetch any entry type with a custom converter.
const article = await contentfulEntryRest(
config,
'articleId123',
myArticleConverter,
{ locale: 'en-US' }
);Converter Pattern
The package uses a functional composition pattern for converting Contentful entries to your domain types.
Base Converter
import { basePageConverter } from '@se-studio/contentful-rest-api';
// Use the base converter
const page = basePageConverter(contentfulEntry);Enhancers
Enhancers are functions that wrap converters to add additional functionality:
import {
basePageConverter,
withSEO,
withSections,
withTags,
compose
} from '@se-studio/contentful-rest-api';
// Compose multiple enhancers
const myConverter = compose(
withSEO,
withSections,
withTags
)(basePageConverter);
// Use with API functions
const page = await contentfulPageRest(config, 'home', {}, myConverter);Custom Converters
Create your own converter enhancers:
import type { Converter, PageEntrySkeleton } from '@se-studio/contentful-rest-api';
import type { IPage } from '@se-studio/core-data-types';
function withCustomField(
converter: Converter<PageEntrySkeleton, IPage>
): Converter<PageEntrySkeleton, IPage> {
return (entry) => {
const page = converter(entry);
// Add custom logic
return {
...page,
customField: entry.fields.customField || 'default'
};
};
}
// Use it
const converter = withCustomField(basePageConverter);Caching
Flat navigation
Menus are assembled in memory from a flat registry of navigationItem / navigation entries plus a bulk internal-link index — no deep Contentful include chain.
Preferred for page renders: set navigationResolution: 'flat' on FetchOptions (e.g. in app buildOptions). Entity fetches stub nav root ids during conversion, then assemble full menu/footer trees post-convert and rebuild icon sprites. Layouts receive a complete model — no layout-time getNavigation bridge.
// app cms-server buildOptions
buildFetchOptions({ navigationResolution: 'flat', ...options }, preview);Ad hoc assembly: call getNavigation(navigationId) from createAppHelpers in @se-studio/core-ui when you only need a menu by id. Do not call registry or link-index fetchers from layouts; those are package-internal.
const menu = await helpers.getNavigation(mainNavId);Default is navigationResolution: 'bundled' (resolve menu/footer via Contentful includes during conversion).
getNavigation applies buildFlatNavigationCacheOptions():
- Tag invalidation (primary): every sub-fetch is tagged with
FLAT_NAV_CACHE_TAGS(navigation, navigationItem, page, article, tag, tagType, link, etc.) so Contentful webhooks can invalidate menus when any routable entry changes. - Time revalidation (fallback):
FLAT_NAV_TIME_REVALIDATE_SECONDS(3600) when a webhook is missed. Override withoptions.next.revalidate(falsefor tag-only caching).
Unresolved internal nav targets render as blank links and log under LOG_CMS=1.
Golden listing fields
Bulk link fetches (fetchProfile: 'listing') use field select lists documented in listing-fetch-params.ts. Spaces that omit optional fields will 422 — trim per content type in the consumer, or omit select for schema-tolerant registry fetches.
tagType link fetches request fields.slug (important for customer URL schemes). Spaces without slug automatically retry with minimal fields (name, show).
Cache Tags
The package automatically manages cache tags for Next.js App Router. Tags follow a consistent naming convention:
- Individual entries:
{contentType}#{slug}(e.g.,page#home,article#my-post) - Collections:
{contentType}(e.g.,page,article,articleType) - Preview mode:
global(single tag for all content when preview mode is enabled)
Publish cascades (coarse): template publish also revalidates page + article; articleType → article; customType → tag + person. See docs/CONTENTFUL_WEBHOOK_REVALIDATION.md.
Automatic Cache Tagging
All content fetching functions automatically apply appropriate cache tags based on the preview flag:
import { contentfulPageRest, contentfulArticleRest } from '@se-studio/contentful-rest-api';
// Production mode: tagged with ['page#home', 'page']
const page = await contentfulPageRest(config, 'home', { preview: false });
// Preview mode: tagged with ['global']
const previewPage = await contentfulPageRest(config, 'home', { preview: true });Preview Mode
When preview: true is passed to fetch functions or revalidation:
- All content uses the
globalcache tag - Revalidation always clears the
globaltag - No time-based revalidation
Cache Revalidation
Webhook Integration
Set up Contentful webhooks to automatically revalidate cache when content changes:
- Create API Route in your Next.js app:
// app/api/revalidate/route.ts
import { createRevalidationHandler } from '@se-studio/contentful-rest-api';
export const POST = createRevalidationHandler({
secret: process.env.REVALIDATION_SECRET,
validateSecret: true,
preview: process.env.DRAFT_ONLY === 'true', // or from your config
});Configure Contentful Webhook:
- URL:
https://yourdomain.com/api/revalidate - Secret: Set
REVALIDATION_SECRETenvironment variable - Triggers: Select content types to monitor (pages, articles, etc.)
- URL:
Required Environment Variables:
REVALIDATION_SECRET=your-webhook-secret
CONTENTFUL_MANAGEMENT_TOKEN=your-management-token
CONTENTFUL_SPACE_ID=your-space-id
CONTENTFUL_ENVIRONMENT_NAME=your-environmentManual Revalidation
Revalidate cache tags programmatically:
'use server'
import { revalidateTags, revalidateSingleTag } from '@se-studio/contentful-rest-api';
// Revalidate multiple tags
await revalidateTags(['page#home', 'page'], 'manual revalidation');
// Revalidate single tag
await revalidateSingleTag('article#my-post', 'article updated');Bulk Revalidation
Trigger full cache revalidation:
# Via HTTP request
curl -X POST https://yourdomain.com/api/revalidate \
-H "REVALIDATE_ALL: true"
# Via single tag
curl -X POST https://yourdomain.com/api/revalidate \
-H "REVALIDATION_TAG: global"Content Type Support
The revalidation system supports these content types:
- Pages:
page(individual:page#{slug}, collection:page) - Page variants:
pageVariant(individual:pageVariant#{slug}, collection:pageVariant) - Articles:
article(individual:article#{slug}, collection:article) - Article Types:
articleType(individual:articleType#{slug}, collection:articleType) - Custom Types:
customType(collection only:customType) - Tags:
tag(individual:tag#{slug}, collection:tag) - Tag types:
tagType(individual:tagType#{slug}, collection:tagType) - Link wrappers:
link(collection:link) - Navigation:
navigation,navigationItem(collection tags; required for flat-nav invalidation) - People:
person(individual:person#{slug}, collection:person) - Templates:
template(individual:template#{cmsLabel}, collection:template) - Banners:
banner(collection:banner) - Assets:
asset(individual:asset#{id}, collection:asset; separate asset webhook in setup scripts) - Locations:
location(individual:location#{slug}, collection:location)
Production webhook filter: use REVALIDATION_WEBHOOK_ENTRY_CONTENT_TYPES or REVALIDATION_WEBHOOK_ENTRY_CONTENT_TYPES_CSV from this package (see docs/CONTENTFUL_WEBHOOK_REVALIDATION.md). redirect is excluded — it uses a Vercel deploy hook.
Tag Functions
Access tag generation functions directly:
import {
pageTag,
articleTag,
articleTypeTag,
PageTag,
ArticleTag,
AllTags
} from '@se-studio/contentful-rest-api';
// Individual tags
const homePageTag = pageTag('home'); // 'page#home'
const blogPostTag = articleTag('my-post'); // 'article#my-post'
// Collection tags
const allPagesTag = PageTag; // 'page'
const allArticlesTag = ArticleTag; // 'article'
// All available tags
console.log(AllTags); // ['page', 'article', 'articleType', ...]Error Handling
The package provides custom error types for different scenarios:
import {
ContentfulError,
RateLimitError,
EntryNotFoundError,
AuthenticationError,
ValidationError,
isRetryableError
} from '@se-studio/contentful-rest-api';
try {
const page = await contentfulPageByIdRest(config, 'invalid-id');
} catch (error) {
if (error instanceof EntryNotFoundError) {
console.log('Entry not found:', error.entryId);
} else if (error instanceof RateLimitError) {
console.log('Rate limited, retry after:', error.retryAfter);
} else if (isRetryableError(error)) {
// Handle retryable errors
}
}Retry Configuration
Configure retry behavior for resilient API calls:
const page = await contentfulPageRest(
config,
'home',
{
retry: {
maxRetries: 3,
initialDelay: 1000,
maxDelay: 30000,
backoffMultiplier: 2
}
}
);Rate Limiting
Use the built-in rate limiter to control request rates:
import { RateLimiter } from '@se-studio/contentful-rest-api';
// Create a rate limiter (5 requests per second)
const limiter = new RateLimiter(5, 1);
// Consume a token before making a request
await limiter.consume();
const page = await contentfulPageRest(config, 'home');Type Generation
Generate TypeScript types from your Contentful space:
1. Configure Environment Variables
Create a .env.local file:
CONTENTFUL_SPACE_ID=your-space-id
CONTENTFUL_MANAGEMENT_TOKEN=your-management-token
CONTENTFUL_ENVIRONMENT_NAME=master2. Generate Types
pnpm --filter @se-studio/contentful-rest-api generate:typesThis creates src/generated/contentful-types.ts with types matching your Contentful content model.
3. Use Generated Types
import type { TypePageSkeleton } from './generated/contentful-types';
import { contentfulEntryRest } from '@se-studio/contentful-rest-api';
const page = await client.getEntry<TypePageSkeleton>('entry-id');Configuration Options
ContentfulConfig
interface ContentfulConfig {
spaceId: string;
accessToken: string;
environment?: string; // defaults to 'master'
host?: string; // for proxies or custom endpoints
options?: Partial<CreateClientParams>;
}FetchOptions
interface FetchOptions {
locale?: string;
preview?: boolean;
include?: number; // include depth for linked entries (default: 10)
cache?: CacheConfig;
retry?: RetryConfig;
enrichPictureBlurPlaceholders?: boolean;
}Picture blur placeholders (enrichPictureBlurPlaceholders) — When true, each included raster IPicture from Contentful (*.ctfassets.net image URLs) triggers an extra cached fetch of a tiny JPEG (width ~24px) to fill blurDataURL for @se-studio/core-ui / next/image. Skip with false or omit (default) to avoid those requests. Reuses the same asset cache tags as other asset enrichment so webhooks can still invalidate. Pictures that already define blurDataURL are left unchanged.
CacheConfig
interface CacheConfig {
tags?: string[]; // Next.js cache tags
revalidate?: number | false; // ISR revalidation time in seconds
cache?: 'force-cache' | 'no-store';
}Verbose logging
| Variable | Output |
|----------|--------|
| LOG_CMS_QUERIES=1 | Structured query logs: operation, slug, payload size, URL ([CMS query] {...}) |
| LOG_CMS_FETCH=1 | Raw Contentful URLs before each fetch (used by smoke tests) |
| LOG_CMS=1 | Conversion/resolution diagnostics and revalidation traces |
Consolidate query logs with Next.js dev cache status:
pnpm dev 2>&1 | tee dev.log
pnpm --filter @se-studio/contentful-rest-api consolidate-cms-logs -- dev.logRecommend logging: { fetches: { fullUrl: true } } in next.config.js so the consolidate script can match URLs reliably.
RetryConfig
interface RetryConfig {
maxRetries?: number; // default: 3
initialDelay?: number; // default: 1000ms
maxDelay?: number; // default: 30000ms
backoffMultiplier?: number; // default: 2
}Primary Tag Selection
By default getPrimaryTag returns tags[0]. Pass a predicate to apply custom selection logic — the library does not prescribe which tag is "primary":
import { getPrimaryTag, type PrimaryTagSelector } from '@se-studio/contentful-rest-api';
// Default: returns tags[0]
const primaryTag = getPrimaryTag(article);
// Custom selection — client supplies the predicate
const mySelector: PrimaryTagSelector = (tag, articleType) =>
articleType?.slug === 'news'
? tag.tagType?.slug === 'disease-area'
: tag.tagType?.slug === 'presentation-type';
const primaryTag = getPrimaryTag(article, mySelector);
// Also fix article.href URL generation via the converter context
const context = createBaseConverterContext({
...config,
primaryTagSelector: mySelector,
});ITagLink carries tagType?: { id, name, slug, show? } so selectors can match on tag type without an extra fetch.
Tag Visual Visibility
show on Tag and TagType controls UI rendering only (cards, detail pages). It does not affect search indexing (indexed), live pages (hidden), or related-articles matching.
import {
filterVisibleTags,
isTagShowEnabled,
isTagVisible,
} from '@se-studio/contentful-rest-api';
// Visible when tag.show and tag.tagType.show are not explicitly false
const displayTags = filterVisibleTags(article.tags);isTagShowEnabled(show)—undefined/null/true=> visible; only explicitfalsehides from UIisTagVisible(tag)— both tag and tag type must not be explicitlyfalsefilterVisibleTags(tags)— filter an article's tags for card/detail display
Link-only tag fetches include fields.show via TAG_LINK_FIELDS so visibility works on listings and cards.
Next.js App Router Integration
Example usage in a Next.js Server Component:
// app/[slug]/page.tsx
import { contentfulPageRest } from '@se-studio/contentful-rest-api';
interface PageProps {
params: { slug: string };
}
export default async function Page({ params }: PageProps) {
const page = await contentfulPageRest(
{
spaceId: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
},
params.slug,
{
cache: {
tags: [`page:${params.slug}`],
revalidate: 3600
}
}
);
if (!page) {
notFound();
}
return (
<div>
<h1>{page.title}</h1>
<p>{page.description}</p>
</div>
);
}
export async function generateStaticParams() {
const pages = await contentfulAllPagesRest({
spaceId: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
});
return pages.map((page) => ({
slug: page.slug,
}));
}API Reference
Main Exports
API Functions
contentfulPageRest- Fetches a page from Contentful by slug; if none exists, resolves a page variant (merged ontooriginalPagewithalternativeContentsswaps) for the same slug. EachalternatePageContententry swapssourceContentforreplacementContents(array, supports 1→N); falls back to singularreplacementContentwhen the array is empty.contentfulArticleRest- Fetches an article by slug and article type slugcontentfulArticleTypeRest- Fetches an article type by slugcontentfulAllPagesRest- Fetches all pages from ContentfulcontentfulAllPageLinks- Fetches all page links plus page variant links (metadata only), deduped by slug (page wins)contentfulAllArticleLinks- Fetches all article links (lightweight metadata only — no full content). ReturnsIContentfulArticleLink[](extendsIArticleLink) includingtitle,subtitle,date,tags,articleType,featuredImage(asvisual),visuals,description, optionaldownload, and optionalsummary(rich text). Used for listing/browsing UIs. Exposed in apps viagetAllArticleLinksfromcreateAppHelpers.filterRelatedArticles- Filters and ranks article links (used by Related Articles collections and full-list helpers). Supports:- Article type (ids or
articleTypeSlugs) - Tags (ids or
tagSlugs; OR match; more matches = higher rank; distinct counting) - Tag types (
tagTypeIds/tagTypeSlugs; hard filter) - Authors (ids or
authorSlugs; score boost by default;strictAuthorMatch: trueturns it into a hard exclude — required for person-scoped News/Pubs lists) - Date range (
before/after),excludeArticleIds,count(undefined = return all),allowUnindexedUsed by@se-studio/core-uigetRelatedArticlesand thegetAllArticleLinks(options)wrapper. Import from main in server code, or via@se-studio/core-ui/server. SeeRelatedArticlesOptionstype.
- Article type (ids or
getBreadcrumbLookup- Fetches a map of path → breadcrumb label for resolving breadcrumb segments from URL paths. Uses same caching as sitemap. Use withresolveBreadcrumbSegmentsfrom@se-studio/core-ui.
Client Functions
createContentfulClient- Creates a Contentful Content Delivery API (CDA) clientcreateContentfulPreviewClient- Creates a Contentful Content Preview API (CPA) clientgetContentfulClient- Gets the appropriate client based on preview mode
Converter Functions
createBaseConverterContext- Creates base converter context with default resolversbasePageConverter- Base converter for pagesbaseArticleConverter- Base converter for articles (full content). ResolvesdownloadvialookupDownloadAssetandurlCalculators.download.baseArticleLinkConverter- Lightweight converter for article list/browse UIs. ReturnsIContentfulArticleLink. Resolvessubtitle,visuals,tags,date,author,description,summary(rich text),download, andfeaturedImagewithout fetching full article content.hrefis the internal detail URL when the article has article-level body content (topContent,content, orbottomContenton the article entry); external-only articles (no article-level body content, withexternalLink) useexternalLinkashref.externalLinkis always preserved on the link object when set. Used internally bycontentfulAllArticleLinks.baseComponentConverter- Base converter for componentsbaseHtmlComponentConverter- Base converter forhtmlComponententries (IBaseHtmlComponent)baseCollectionConverter- Base converter for collections
Revalidation Functions
revalidateTags- Revalidates multiple Next.js cache tagsrevalidateSingleTag- Revalidates a single cache tagcreateRevalidationHandler- Creates Next.js API route handler for webhook revalidationgetCacheTags- Gets cache tags based on content type and preview mode
Error Types
ContentfulError- Base error class for Contentful API errorsRateLimitError- Error for rate limit exceededEntryNotFoundError- Error for entry not foundAuthenticationError- Error for authentication failuresValidationError- Error for validation failures
Utility Functions
isRetryableError- Check if an error is retryablewithRetry- Retry function with exponential backoffRateLimiter- Rate limiter for controlling request rates
For detailed JSDoc documentation on all exports, see the TypeScript declaration files (.d.ts) in the package.
License
MIT
Repository
https://github.com/Something-Else-Studio/se-core-product
