Skip to content

feat: add packages/client with fetchExperience and delivery client [AIS-147]#30

Merged
Lisa White (whitelisab) merged 14 commits into
mainfrom
feat/client-fetch
Jul 14, 2026
Merged

feat: add packages/client with fetchExperience and delivery client [AIS-147]#30
Lisa White (whitelisab) merged 14 commits into
mainfrom
feat/client-fetch

Conversation

@whitelisab

@whitelisab Lisa White (whitelisab) commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

AIS-147 · AIS-148

Summary

  • Adds packages/client (@contentful/experiences-client) — a new internal workspace package that owns the @contentful/experience-delivery dependency and exposes fetchExperience, createClient, ContentfulViewDeliveryClient, and NotFoundError
  • Re-exports the surface from @contentful/experiences-react and @contentful/experiences-svelte so customers install only the framework adapter; @contentful/experience-delivery is no longer a direct customer dep
  • Inlines fetchExperience directly into both example apps; removes the delivery-client.ts wrapper files that added no value

fetchExperience

Function that fetches an Experience payload from Contentful's Experience Delivery API (XDA) and resolves it into a runtime-neutral PortableRenderPlan ready to hand to a renderer, all in one call.

await fetchExperience(
  { spaceId, environmentId, experienceId, locale },  // arg 1: which experience
  { accessToken, host } | { client },                 // arg 2: how to fetch
  { config, context },                                // arg 3: how to resolve
);
  • clientOptions is a discriminated union — inline creds ({ accessToken, host? }) or a pre-made client ({ client })
  • host replaces the old preview: boolean. It's a full base-URL string that passes straight through to the delivery client's baseUrl — the SDK doesn't own XDN/XPA URL constants, callers pick the endpoint
  • createClient({ accessToken, host? }) is a functional constructor that maps the SDK's option names to the underlying delivery client (accessToken → token, host → baseUrl). Prefer over new ContentfulViewDeliveryClient(...) so the two paths through fetchExperience use the same field names

How this fits with the other draft PRs

  • Optimization/personalization spike: That PR adds getExperienceWithOverrides usage and sourceMap threading to wire in the Optimization SDK. This PR intentionally omits the personalization path, but when it's ready to be implemented, resolveOptions (arg 3) can add a personalization field that dispatches to getExperienceWithOverrides. The call sites for other args stay unchanged for customers who don't use it.

  • Preview SDK (three-layer workspace packages): The preview POC adds enablePreview on ExperienceRenderer and a postMessage transport layer. The live editor preview wire in the PR is a separate concern handled by the renderer after hydration and is fully compatible with this PR.

Test plan

  • npm run build — all 5 packages build cleanly (NX topological order: core → design → client → adapters)
  • npx vitest run packages/client — 11 tests pass (create-client name mapping, XDN default, host passthrough, pre-created-client passthrough, empty-nodes resolves to a plan, NotFoundError propagates)
  • Confirm @contentful/experience-delivery absent from examples/nextjs/package.json and examples/sveltekit/package.json
  • Confirm fetchExperience, createClient, NotFoundError, and ContentfulViewDeliveryClient importable from @contentful/experiences-react
  • Confirm the same imports work from @contentful/experiences-svelte
  • Exercise both example apps: happy path renders, bad slug 404s, empty-nodes slug renders as empty page

Generated with Claude Code

Lisa White (whitelisab) and others added 10 commits July 10, 2026 15:12
…ent re-export

Creates the packages/client workspace package (@contentful/experiences-client).
Replaces the ExperienceClient class approach from PR #27 — re-exports
ContentfulViewDeliveryClient directly from @contentful/experience-delivery so
consumers have full client access without a separate install.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ture

fetchExperience accepts either inline credentials ({ accessToken, preview?,
spaceId, environmentId, experienceId }) or a pre-created
ContentfulViewDeliveryClient ({ client, spaceId, environmentId, experienceId }).

Inline path constructs the client internally with XDN or XPA base URL based
on the preview flag. Pre-created client path is pass-through, enabling reuse
across calls. When personalization is passed, dispatches to
getExperienceWithOverrides and opts into sourceMap automatically.

Returns PortableRenderPlan | null (null when payload has no nodes).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ience

Both @contentful/experiences-react and @contentful/experiences-svelte now
re-export ContentfulViewDeliveryClient and fetchExperience from
@contentful/experiences-client, so consumers install only the framework
adapter and never touch @contentful/experience-delivery or
@contentful/experiences-client directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tful/experience-delivery

Both example apps now import fetchExperience and ContentfulViewDeliveryClient
from their framework adapter rather than @contentful/experience-delivery
directly. The separate resolveExperience call is removed — fetch and resolve
happen in one step via fetchExperience. delivery-client.ts in each example
wraps the SDK call with a thin page-level helper that reads env vars and
passes the config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… delivery-client wrappers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ttier

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…elivery namespace for request type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nce unconditionally

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall really liking this approach, nice work 🚀 Especially the idea of moving resolveExperience into the fetchExperience function.

Comment thread packages/client/src/fetch-experience.ts Outdated
locale,
})) as unknown as ExperiencePayload;

if (!payload?.nodes?.length) return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to only return null here or throw an error? Other option could be still return null but console.warn to let the developer know the nodes were empty.

I could go either way on leaving as is or being more opinionated that this is an error. Let me know your thoughts!

@whitelisab Lisa White (whitelisab) Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating to remove this line, and instead if there is an empty experience, that is just passed through the resolver. I also am updating the examples to show better error handling for the case when there's an error fetching the experience

context?: ResolveExperienceOptions['experience'];
} & ({ accessToken: string; preview?: boolean } | { client: ContentfulViewDeliveryClient });

export async function fetchExperience(

@chasepoirier Chase Poirier (chasepoirier) Jul 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really liking how this feels to have resolve experience bundled into this helper function, feels like a very smooth DX since they don't need two separate async function calls. Very nice!

Some food for thought on this function: As this function now does more, the amount of params is growing and it may grow even more when personalization and the channels/digital properties RFC is added into the picture.

One idea we could explore is having multiple params and grouping like-params together instead of one large object. An idea here could be 3 params with param 1 being the "identifier" for the experience to fetch (idea for separating this is because maybe this because a "digital property" object in the future compared to just an experienceId), param 2 is the client and param 3 is all experience related config

Example:

// this can also be in a separate file
const client = createClient(/** all client params here */)

const experience = fetchExperience(experienceId, client, { 
  config,
  context
})

Let me know your thoughts!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that the params for this function will increase as we start adding support for other features. I'm in support of grouping like-parms together 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on this feedback I've grouped them into three concerns: which experience to fetch, how to fetch, and how to resolve

Comment thread packages/client/src/index.ts Outdated
@@ -0,0 +1,3 @@
export { ContentfulViewDeliveryClient } from '@contentful/experience-delivery';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think @contentful/experience-delivery currently exposes a createClient type of function to instantiate a new client.

Thoughts on adding an additional helper here to do so? This would be a similar pattern to what the CMA does with something like:

// this can also be in a separate file
const client = createClient(/** all client params here */)

Mainly would be an add for DX preference in case devs would rather use a functional approach vs doing

const client = new ContentfulViewDeliveryClient()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I like adding createClient, I agree that I think it improves the DX. I didn't see anything in the client for that, so I think we can add it here for now, and then could consider if it's worth adding it directly to the client in the future

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the createClient function and ensure that it uses the same param names as fetchExperience, mapping them to the proper names for the experience delivery client. Also updated so that if no host/baseURL is passed in, then that's handled in the experience delivery client to use the default instead of having that logic in this SDK.

Lisa White (whitelisab) and others added 4 commits July 13, 2026 14:44
…ce preview with host

Signature is now `fetchExperience(experienceOptions, clientOptions, resolveOptions)`,
with `ClientOptions` a discriminated union of `{ accessToken, host? }` or `{ client }`.
`host` is a full base URL string that's passed through as `baseUrl`; XPA vs XDN
endpoint selection is now the caller's concern. Defaults to XDN when omitted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…en/baseUrl

createClient is a functional constructor over ContentfulViewDeliveryClient
that maps the SDK's option names (accessToken, host) to the delivery client's
underlying names (token, baseUrl). Other options pass through unchanged.

fetchExperience's inline-creds branch now routes through createClient, making
it the single source of truth for the name mapping.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…se NotFoundError

fetchExperience no longer treats an empty-nodes payload as "not found" — the
resolver handles empty nodes gracefully and returns a valid PortableRenderPlan.
Return type narrows from `PortableRenderPlan | null` to `PortableRenderPlan`.

To keep the missing-experience case as a proper 404 (not a 500), NotFoundError
is now re-exported from the client + adapter packages. Example call sites
wrap fetchExperience in try/catch and route NotFoundError to their framework's
404 idiom (notFound() in Next, error(404) in SvelteKit).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Covers the three review-driven changes:
- 3 positional args (experienceOptions, clientOptions, resolveOptions)
- createClient helper + host string in place of preview boolean
- Empty-nodes payloads pass through; NotFoundError re-exported for the
  missing-experience case

Adds design-decision entries in AGENTS.md capturing the *why* behind each
choice so future contributors don't re-litigate them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌

@whitelisab
Lisa White (whitelisab) marked this pull request as ready for review July 14, 2026 03:57
@whitelisab
Lisa White (whitelisab) merged commit bc9f034 into main Jul 14, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants