npm package: @se-studio/contentful-rest-api
Description: Type-safe Contentful REST API client with caching and rate limiting for Next.js applications
Latest version: 1.0.128

# @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](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
  - [Basic Usage](#basic-usage)
  - [Preview Mode](#preview-mode)
- [API Reference](#api-reference)
  - [Client Functions](#client-functions)
  - [Content Fetching Functions](#content-fetching-functions)
- [Converter Pattern](#converter-pattern)
  - [Base Converter](#base-converter)
  - [Enhancers](#enhancers)
  - [Custom Converters](#custom-converters)
- [Caching](#caching)
  - [Cache Tags](#cache-tags)
  - [Cache Revalidation](#cache-revalidation)
- [Error Handling](#error-handling)
- [Retry Configuration](#retry-configuration)
- [Rate Limiting](#rate-limiting)
- [Type Generation](#type-generation)
- [Configuration Options](#configuration-options)
  - [ContentfulConfig](#contentfulconfig)
  - [FetchOptions](#fetchoptions)
  - [CacheConfig](#cacheconfig)
  - [RetryConfig](#retryconfig)
- [Next.js App Router Integration](#nextjs-app-router-integration)
- [License](#license)
- [Repository](#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
- 🛡️ **Error Handling** - Custom error types for different failure scenarios

## Installation

```bash
pnpm add @se-studio/contentful-rest-api @se-studio/core-data-types contentful
```

## Quick Start

### Basic Usage

```typescript
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

```typescript
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.

```typescript
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.

```typescript
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>`

```typescript
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

```typescript
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[]>`

```typescript
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.

```typescript
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

```typescript
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:

```typescript
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:

```typescript
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

### 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)

### Automatic Cache Tagging

All content fetching functions automatically apply appropriate cache tags based on the `preview` flag:

```typescript
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:

```typescript
// 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
});
```

2. **Configure Contentful Webhook**:
   - URL: `https://yourdomain.com/api/revalidate`
   - Secret: Set `REVALIDATION_SECRET` environment variable
   - Triggers: Select content types to monitor (pages, articles, etc.)

3. **Required Environment Variables**:
```bash
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:

```typescript
'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:

```bash
# 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`)
- **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`)
- **People**: `person` (individual: `person#{slug}`, collection: `person`)
- **Assets**: `asset` (individual: `asset#{id}`, collection: `asset`)
- **Locations**: `location` (individual: `location#{slug}`, collection: `location`)

### Tag Functions

Access tag generation functions directly:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```env
CONTENTFUL_SPACE_ID=your-space-id
CONTENTFUL_MANAGEMENT_TOKEN=your-management-token
CONTENTFUL_ENVIRONMENT_NAME=master
```

### 2. Generate Types

```bash
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

```typescript
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

```typescript
interface ContentfulConfig {
  spaceId: string;
  accessToken: string;
  environment?: string; // defaults to 'master'
  host?: string; // for proxies or custom endpoints
  options?: Partial<CreateClientParams>;
}
```

### FetchOptions

```typescript
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

```typescript
interface CacheConfig {
  tags?: string[]; // Next.js cache tags
  revalidate?: number | false; // ISR revalidation time in seconds
  cache?: 'force-cache' | 'no-store';
}
```

### RetryConfig

```typescript
interface RetryConfig {
  maxRetries?: number; // default: 3
  initialDelay?: number; // default: 1000ms
  maxDelay?: number; // default: 30000ms
  backoffMultiplier?: number; // default: 2
}
```

## Next.js App Router Integration

Example usage in a Next.js Server Component:

```typescript
// 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
- **`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`, and optional `summary` (rich text). Used for listing/browsing UIs. Exposed in apps via `getAllArticleLinks` from `createAppHelpers`.
- **`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)
- **`baseArticleLinkConverter`** - Lightweight converter for article list/browse UIs. Returns `IContentfulArticleLink`. Resolves `subtitle`, `visuals`, `tags`, `date`, `author`, `description`, `summary` (rich text), and `featuredImage` without fetching full article content. 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