@se-studio/contentful-rest-api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,467 @@
1
+ # @se-studio/contentful-rest-api
2
+
3
+ Type-safe Contentful REST API client with caching, rate limiting, and extensible converters for Next.js applications.
4
+
5
+ ## Features
6
+
7
+ - 🎯 **Type-safe** - Full TypeScript support with generated types from your Contentful space
8
+ - 🔄 **Dual API Support** - Content Delivery API (CDA) and Content Preview API (CPA)
9
+ - ⚡ **Next.js Optimized** - Built-in support for Next.js App Router cache tags and revalidation
10
+ - 🔁 **Retry Logic** - Automatic retry with exponential backoff for failed requests
11
+ - 🚦 **Rate Limiting** - Respect Contentful API rate limits with built-in rate limiter
12
+ - 🧩 **Extensible Converters** - Functional composition pattern for customizing content transformations
13
+ - 🛡️ **Error Handling** - Custom error types for different failure scenarios
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pnpm add @se-studio/contentful-rest-api @se-studio/core-data-types contentful
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### Basic Usage
24
+
25
+ ```typescript
26
+ import { contentfulPageRest } from '@se-studio/contentful-rest-api';
27
+
28
+ // Fetch a page by slug
29
+ const page = await contentfulPageRest(
30
+ {
31
+ spaceId: process.env.CONTENTFUL_SPACE_ID!,
32
+ accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
33
+ environment: 'master'
34
+ },
35
+ 'home',
36
+ {
37
+ locale: 'en-US',
38
+ cache: {
39
+ tags: ['home-page'],
40
+ revalidate: 3600 // 1 hour
41
+ }
42
+ }
43
+ );
44
+ ```
45
+
46
+ ### Preview Mode
47
+
48
+ ```typescript
49
+ import { contentfulPageRest } from '@se-studio/contentful-rest-api';
50
+
51
+ // Use preview API for draft content
52
+ const page = await contentfulPageRest(
53
+ {
54
+ spaceId: process.env.CONTENTFUL_SPACE_ID!,
55
+ accessToken: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN!,
56
+ },
57
+ 'home',
58
+ {
59
+ preview: true, // Uses Preview API
60
+ cache: { cache: 'no-store' } // Don't cache preview content
61
+ }
62
+ );
63
+ ```
64
+
65
+ ## API Reference
66
+
67
+ ### Client Functions
68
+
69
+ #### `createContentfulClient(config)`
70
+
71
+ Creates a Contentful Content Delivery API (CDA) client.
72
+
73
+ ```typescript
74
+ import { createContentfulClient } from '@se-studio/contentful-rest-api';
75
+
76
+ const client = createContentfulClient({
77
+ spaceId: process.env.CONTENTFUL_SPACE_ID!,
78
+ accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
79
+ environment: 'master'
80
+ });
81
+ ```
82
+
83
+ #### `createContentfulPreviewClient(config)`
84
+
85
+ Creates a Contentful Content Preview API (CPA) client.
86
+
87
+ ```typescript
88
+ import { createContentfulPreviewClient } from '@se-studio/contentful-rest-api';
89
+
90
+ const previewClient = createContentfulPreviewClient({
91
+ spaceId: process.env.CONTENTFUL_SPACE_ID!,
92
+ accessToken: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN!,
93
+ });
94
+ ```
95
+
96
+ ### Content Fetching Functions
97
+
98
+ #### `contentfulPageRest(config, slug, options?, converter?)`
99
+
100
+ Fetches a page by slug.
101
+
102
+ **Parameters:**
103
+ - `config`: ContentfulConfig - Contentful client configuration
104
+ - `slug`: string - Page slug to fetch
105
+ - `options?`: FetchOptions - Optional fetch options (locale, preview, cache, retry)
106
+ - `converter?`: Converter - Optional custom converter function
107
+
108
+ **Returns:** `Promise<IPage | null>`
109
+
110
+ ```typescript
111
+ const page = await contentfulPageRest(
112
+ config,
113
+ 'about-us',
114
+ {
115
+ locale: 'en-US',
116
+ include: 10,
117
+ cache: { tags: ['about-page'], revalidate: 3600 }
118
+ }
119
+ );
120
+ ```
121
+
122
+ #### `contentfulPageByIdRest(config, id, options?, converter?)`
123
+
124
+ Fetches a page by entry ID.
125
+
126
+ **Parameters:**
127
+ - `config`: ContentfulConfig
128
+ - `id`: string - Entry ID
129
+ - `options?`: FetchOptions
130
+ - `converter?`: Converter
131
+
132
+ **Returns:** `Promise<IPage>`
133
+
134
+ **Throws:** `EntryNotFoundError` if entry doesn't exist
135
+
136
+ ```typescript
137
+ const page = await contentfulPageByIdRest(
138
+ config,
139
+ '5nZHNlP9rZhWvKx4w2Z8zB'
140
+ );
141
+ ```
142
+
143
+ #### `contentfulAllPagesRest(config, options?, converter?)`
144
+
145
+ Fetches all pages from Contentful.
146
+
147
+ **Parameters:**
148
+ - `config`: ContentfulConfig
149
+ - `options?`: FetchOptions
150
+ - `converter?`: Converter
151
+
152
+ **Returns:** `Promise<IPage[]>`
153
+
154
+ ```typescript
155
+ const pages = await contentfulAllPagesRest(
156
+ config,
157
+ {
158
+ locale: 'en-US',
159
+ cache: { tags: ['all-pages'], revalidate: 3600 }
160
+ }
161
+ );
162
+ ```
163
+
164
+ #### `contentfulEntryRest<TEntry, TResult>(config, id, converter, options?)`
165
+
166
+ Generic function to fetch any entry type with a custom converter.
167
+
168
+ ```typescript
169
+ const article = await contentfulEntryRest(
170
+ config,
171
+ 'articleId123',
172
+ myArticleConverter,
173
+ { locale: 'en-US' }
174
+ );
175
+ ```
176
+
177
+ ## Converter Pattern
178
+
179
+ The package uses a functional composition pattern for converting Contentful entries to your domain types.
180
+
181
+ ### Base Converter
182
+
183
+ ```typescript
184
+ import { basePageConverter } from '@se-studio/contentful-rest-api';
185
+
186
+ // Use the base converter
187
+ const page = basePageConverter(contentfulEntry);
188
+ ```
189
+
190
+ ### Enhancers
191
+
192
+ Enhancers are functions that wrap converters to add additional functionality:
193
+
194
+ ```typescript
195
+ import {
196
+ basePageConverter,
197
+ withSEO,
198
+ withSections,
199
+ withTags,
200
+ compose
201
+ } from '@se-studio/contentful-rest-api';
202
+
203
+ // Compose multiple enhancers
204
+ const myConverter = compose(
205
+ withSEO,
206
+ withSections,
207
+ withTags
208
+ )(basePageConverter);
209
+
210
+ // Use with API functions
211
+ const page = await contentfulPageRest(config, 'home', {}, myConverter);
212
+ ```
213
+
214
+ ### Custom Converters
215
+
216
+ Create your own converter enhancers:
217
+
218
+ ```typescript
219
+ import type { Converter, PageEntrySkeleton } from '@se-studio/contentful-rest-api';
220
+ import type { IPage } from '@se-studio/core-data-types';
221
+
222
+ function withCustomField(
223
+ converter: Converter<PageEntrySkeleton, IPage>
224
+ ): Converter<PageEntrySkeleton, IPage> {
225
+ return (entry) => {
226
+ const page = converter(entry);
227
+
228
+ // Add custom logic
229
+ return {
230
+ ...page,
231
+ customField: entry.fields.customField || 'default'
232
+ };
233
+ };
234
+ }
235
+
236
+ // Use it
237
+ const converter = withCustomField(basePageConverter);
238
+ ```
239
+
240
+ ## Caching
241
+
242
+ ### Cache Tags
243
+
244
+ The package provides utilities for generating cache tags for Next.js:
245
+
246
+ ```typescript
247
+ import { generateCacheTags, getPageCacheTags } from '@se-studio/contentful-rest-api';
248
+
249
+ // Generate tags for a content type
250
+ const tags = generateCacheTags('page', 'entry-id');
251
+ // Returns: ['contentful', 'contentful:page', 'contentful:page:entry-id']
252
+
253
+ // Get page-specific cache tags
254
+ const pageTags = getPageCacheTags('home', 'en-US');
255
+ ```
256
+
257
+ ### Cache Revalidation
258
+
259
+ Revalidate cache tags in Server Actions or Route Handlers:
260
+
261
+ ```typescript
262
+ 'use server'
263
+
264
+ import { revalidateTags } from '@se-studio/contentful-rest-api';
265
+
266
+ export async function revalidateHomePage() {
267
+ await revalidateTags(['contentful:page:home']);
268
+ }
269
+ ```
270
+
271
+ ## Error Handling
272
+
273
+ The package provides custom error types for different scenarios:
274
+
275
+ ```typescript
276
+ import {
277
+ ContentfulError,
278
+ RateLimitError,
279
+ EntryNotFoundError,
280
+ AuthenticationError,
281
+ ValidationError,
282
+ isRetryableError
283
+ } from '@se-studio/contentful-rest-api';
284
+
285
+ try {
286
+ const page = await contentfulPageByIdRest(config, 'invalid-id');
287
+ } catch (error) {
288
+ if (error instanceof EntryNotFoundError) {
289
+ console.log('Entry not found:', error.entryId);
290
+ } else if (error instanceof RateLimitError) {
291
+ console.log('Rate limited, retry after:', error.retryAfter);
292
+ } else if (isRetryableError(error)) {
293
+ // Handle retryable errors
294
+ }
295
+ }
296
+ ```
297
+
298
+ ## Retry Configuration
299
+
300
+ Configure retry behavior for resilient API calls:
301
+
302
+ ```typescript
303
+ const page = await contentfulPageRest(
304
+ config,
305
+ 'home',
306
+ {
307
+ retry: {
308
+ maxRetries: 3,
309
+ initialDelay: 1000,
310
+ maxDelay: 30000,
311
+ backoffMultiplier: 2
312
+ }
313
+ }
314
+ );
315
+ ```
316
+
317
+ ## Rate Limiting
318
+
319
+ Use the built-in rate limiter to control request rates:
320
+
321
+ ```typescript
322
+ import { RateLimiter } from '@se-studio/contentful-rest-api';
323
+
324
+ // Create a rate limiter (5 requests per second)
325
+ const limiter = new RateLimiter(5, 1);
326
+
327
+ // Consume a token before making a request
328
+ await limiter.consume();
329
+ const page = await contentfulPageRest(config, 'home');
330
+ ```
331
+
332
+ ## Type Generation
333
+
334
+ Generate TypeScript types from your Contentful space:
335
+
336
+ ### 1. Configure Environment Variables
337
+
338
+ Create a `.env.local` file:
339
+
340
+ ```env
341
+ CONTENTFUL_SPACE_ID=your-space-id
342
+ CONTENTFUL_MANAGEMENT_TOKEN=your-management-token
343
+ CONTENTFUL_ENVIRONMENT_NAME=master
344
+ ```
345
+
346
+ ### 2. Generate Types
347
+
348
+ ```bash
349
+ pnpm --filter @se-studio/contentful-rest-api generate:types
350
+ ```
351
+
352
+ This creates `src/generated/contentful-types.ts` with types matching your Contentful content model.
353
+
354
+ ### 3. Use Generated Types
355
+
356
+ ```typescript
357
+ import type { TypePageSkeleton } from './generated/contentful-types';
358
+ import { contentfulEntryRest } from '@se-studio/contentful-rest-api';
359
+
360
+ const page = await client.getEntry<TypePageSkeleton>('entry-id');
361
+ ```
362
+
363
+ ## Configuration Options
364
+
365
+ ### ContentfulConfig
366
+
367
+ ```typescript
368
+ interface ContentfulConfig {
369
+ spaceId: string;
370
+ accessToken: string;
371
+ environment?: string; // defaults to 'master'
372
+ host?: string; // for proxies or custom endpoints
373
+ options?: Partial<CreateClientParams>;
374
+ }
375
+ ```
376
+
377
+ ### FetchOptions
378
+
379
+ ```typescript
380
+ interface FetchOptions {
381
+ locale?: string;
382
+ preview?: boolean;
383
+ include?: number; // include depth for linked entries (default: 10)
384
+ cache?: CacheConfig;
385
+ retry?: RetryConfig;
386
+ }
387
+ ```
388
+
389
+ ### CacheConfig
390
+
391
+ ```typescript
392
+ interface CacheConfig {
393
+ tags?: string[]; // Next.js cache tags
394
+ revalidate?: number | false; // ISR revalidation time in seconds
395
+ cache?: 'force-cache' | 'no-store';
396
+ }
397
+ ```
398
+
399
+ ### RetryConfig
400
+
401
+ ```typescript
402
+ interface RetryConfig {
403
+ maxRetries?: number; // default: 3
404
+ initialDelay?: number; // default: 1000ms
405
+ maxDelay?: number; // default: 30000ms
406
+ backoffMultiplier?: number; // default: 2
407
+ }
408
+ ```
409
+
410
+ ## Next.js App Router Integration
411
+
412
+ Example usage in a Next.js Server Component:
413
+
414
+ ```typescript
415
+ // app/[slug]/page.tsx
416
+ import { contentfulPageRest } from '@se-studio/contentful-rest-api';
417
+
418
+ interface PageProps {
419
+ params: { slug: string };
420
+ }
421
+
422
+ export default async function Page({ params }: PageProps) {
423
+ const page = await contentfulPageRest(
424
+ {
425
+ spaceId: process.env.CONTENTFUL_SPACE_ID!,
426
+ accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
427
+ },
428
+ params.slug,
429
+ {
430
+ cache: {
431
+ tags: [`page:${params.slug}`],
432
+ revalidate: 3600
433
+ }
434
+ }
435
+ );
436
+
437
+ if (!page) {
438
+ notFound();
439
+ }
440
+
441
+ return (
442
+ <div>
443
+ <h1>{page.title}</h1>
444
+ <p>{page.description}</p>
445
+ </div>
446
+ );
447
+ }
448
+
449
+ export async function generateStaticParams() {
450
+ const pages = await contentfulAllPagesRest({
451
+ spaceId: process.env.CONTENTFUL_SPACE_ID!,
452
+ accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
453
+ });
454
+
455
+ return pages.map((page) => ({
456
+ slug: page.slug,
457
+ }));
458
+ }
459
+ ```
460
+
461
+ ## License
462
+
463
+ MIT
464
+
465
+ ## Repository
466
+
467
+ https://github.com/Something-Else-Studio/se-core-product