npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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

  • 🎯 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 src URLs use Contentful Images API w/fm/q so next/image never 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 contentful

Quick 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 configuration
  • slug: string - Page slug to fetch
  • options?: 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: ContentfulConfig
  • id: string - Entry ID
  • options?: FetchOptions
  • converter?: 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: ContentfulConfig
  • options?: FetchOptions
  • converter?: 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 with options.next.revalidate (false for 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; articleTypearticle; customTypetag + 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 global cache tag
  • Revalidation always clears the global tag
  • No time-based revalidation

Cache Revalidation

Webhook Integration

Set up Contentful webhooks to automatically revalidate cache when content changes:

  1. 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
});
  1. Configure Contentful Webhook:

    • URL: https://yourdomain.com/api/revalidate
    • Secret: Set REVALIDATION_SECRET environment variable
    • Triggers: Select content types to monitor (pages, articles, etc.)
  2. Required Environment Variables:

REVALIDATION_SECRET=your-webhook-secret
CONTENTFUL_MANAGEMENT_TOKEN=your-management-token
CONTENTFUL_SPACE_ID=your-space-id
CONTENTFUL_ENVIRONMENT_NAME=your-environment

Manual 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=master

2. Generate Types

pnpm --filter @se-studio/contentful-rest-api generate:types

This 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.log

Recommend 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 explicit false hides from UI
  • isTagVisible(tag) — both tag and tag type must not be explicitly false
  • filterVisibleTags(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 onto originalPage with alternativeContents swaps) for the same slug. Each alternatePageContent entry swaps sourceContent for replacementContents (array, supports 1→N); falls back to singular replacementContent when the array is empty.
  • contentfulArticleRest - Fetches an article by slug and article type slug
  • contentfulArticleTypeRest - Fetches an article type by slug
  • contentfulAllPagesRest - Fetches all pages from Contentful
  • contentfulAllPageLinks - 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). Returns IContentfulArticleLink[] (extends IArticleLink) including title, subtitle, date, tags, articleType, featuredImage (as visual), visuals, description, optional download, and optional summary (rich text). Used for listing/browsing UIs. Exposed in apps via getAllArticleLinks from createAppHelpers.
  • 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: true turns it into a hard exclude — required for person-scoped News/Pubs lists)
    • Date range (before / after), excludeArticleIds, count (undefined = return all), allowUnindexed Used by @se-studio/core-ui getRelatedArticles and the getAllArticleLinks(options) wrapper. Import from main in server code, or via @se-studio/core-ui/server. See RelatedArticlesOptions type.
  • getBreadcrumbLookup - Fetches a map of path → breadcrumb label for resolving breadcrumb segments from URL paths. Uses same caching as sitemap. Use with resolveBreadcrumbSegments from @se-studio/core-ui.

Client Functions

  • createContentfulClient - Creates a Contentful Content Delivery API (CDA) client
  • createContentfulPreviewClient - Creates a Contentful Content Preview API (CPA) client
  • getContentfulClient - Gets the appropriate client based on preview mode

Converter Functions

  • createBaseConverterContext - Creates base converter context with default resolvers
  • basePageConverter - Base converter for pages
  • baseArticleConverter - Base converter for articles (full content). Resolves download via lookupDownloadAsset and urlCalculators.download.
  • baseArticleLinkConverter - Lightweight converter for article list/browse UIs. Returns IContentfulArticleLink. Resolves subtitle, visuals, tags, date, author, description, summary (rich text), download, and featuredImage without fetching full article content. href is the internal detail URL when the article has article-level body content (topContent, content, or bottomContent on the article entry); external-only articles (no article-level body content, with externalLink) use externalLink as href. externalLink is always preserved on the link object when set. Used internally by contentfulAllArticleLinks.
  • baseComponentConverter - Base converter for components
  • baseHtmlComponentConverter - Base converter for htmlComponent entries (IBaseHtmlComponent)
  • baseCollectionConverter - Base converter for collections

Revalidation Functions

  • revalidateTags - Revalidates multiple Next.js cache tags
  • revalidateSingleTag - Revalidates a single cache tag
  • createRevalidationHandler - Creates Next.js API route handler for webhook revalidation
  • getCacheTags - Gets cache tags based on content type and preview mode

Error Types

  • ContentfulError - Base error class for Contentful API errors
  • RateLimitError - Error for rate limit exceeded
  • EntryNotFoundError - Error for entry not found
  • AuthenticationError - Error for authentication failures
  • ValidationError - Error for validation failures

Utility Functions

  • isRetryableError - Check if an error is retryable
  • withRetry - Retry function with exponential backoff
  • RateLimiter - 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