Skip to content

feat(capi-client): add typed Content Delivery API client package#428

Open
maoberlehner wants to merge 26 commits intomainfrom
feature/capi-client
Open

feat(capi-client): add typed Content Delivery API client package#428
maoberlehner wants to merge 26 commits intomainfrom
feature/capi-client

Conversation

@maoberlehner
Copy link
Contributor

Introduce a new @storyblok/api-client package that provides a modern, typed TS client for the Content Delivery (CAPI) stories endpoints, generated from our OpenAPI spec. This improves DX and consistency when consuming the Stories API.

  • Add OpenAPI CAPI stories spec and shared schemas for story field types
  • Generate typed CAPI client (fetch-based) and SDK with get and getAll helpers
  • Implement createApiClient wrapper with region-aware base URL and access token handling
  • Add basic tests using MSW + OpenAPI mocks to validate stories.get and stories.getAll
  • Configure build (tsdown), linting, testing, and npm packaging for the new package

Fixes WDX-281

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new typed @storyblok/api-client package generated from an OpenAPI spec for the CAPI Stories endpoints, plus shared story schemas and supporting build/test tooling.

Changes:

  • Added/expanded OpenAPI spec files for CAPI Stories + shared story/object field type schemas.
  • Introduced a generated, fetch-based TypeScript client wrapper with stories.get / stories.getAll.
  • Added package tooling (tsdown, eslint, vitest) and basic MSW/OpenAPI-driven tests.

Reviewed changes

Copilot reviewed 35 out of 36 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
packages/openapi/resources/shared/stories/story-translated-slug.yaml Adds shared schema for translated slugs in story objects.
packages/openapi/resources/shared/stories/story-localized-path.yaml Adds shared schema for localized paths in story objects.
packages/openapi/resources/shared/stories/story-content.yaml Defines output schema for story.content and supported field types.
packages/openapi/resources/shared/stories/story-content-input.yaml Defines input schema for story.content (create/update).
packages/openapi/resources/shared/stories/story-base.yaml Adds common story fields used across APIs.
packages/openapi/resources/shared/stories/story-alternate.yaml Adds minimal schema for alternate stories.
packages/openapi/resources/shared/stories/field-types/table-field.yaml Adds schema for Storyblok table field output.
packages/openapi/resources/shared/stories/field-types/table-field-input.yaml Adds schema for Storyblok table field input.
packages/openapi/resources/shared/stories/field-types/richtext-field.yaml Adds schema for Storyblok richtext field output.
packages/openapi/resources/shared/stories/field-types/richtext-field-input.yaml Adds schema for Storyblok richtext field input.
packages/openapi/resources/shared/stories/field-types/plugin-field.yaml Adds schema for plugin/custom fields output.
packages/openapi/resources/shared/stories/field-types/plugin-field-input.yaml Adds schema for plugin/custom fields input.
packages/openapi/resources/shared/stories/field-types/multilink-field.yaml Adds multilink field output schema.
packages/openapi/resources/shared/stories/field-types/multilink-field-input.yaml Adds multilink field input schema.
packages/openapi/resources/shared/stories/field-types/asset-field.yaml Adds asset field output schema.
packages/openapi/resources/shared/stories/field-types/asset-field-input.yaml Adds asset field input schema.
packages/openapi/resources/shared/responses.yaml Introduces shared error response schemas.
packages/openapi/resources/shared/parameters.yaml Introduces shared path parameter definitions.
packages/openapi/resources/shared/pagination.yaml Introduces shared pagination param/header definitions.
packages/openapi/resources/capi/stories/main.yaml Adds OpenAPI spec for CAPI stories endpoints.
packages/openapi/resources/capi/shared/stories/story-capi.yaml Defines CAPI-specific story schema that composes story-base.
packages/openapi/resources/capi/shared/servers.yaml Adds region-specific server base URLs.
packages/openapi/resources/capi/shared/security.yaml Adds security requirement definition for CAPI spec.
packages/openapi/resources/capi/shared/security-schemes.yaml Adds security scheme for token query auth.
packages/openapi/redocly.yaml Registers the new CAPI Stories OpenAPI build output.
packages/capi-client/vitest.config.ts Configures Vitest for the new client package.
packages/capi-client/tsdown.config.ts Configures tsdown build for ESM/CJS + d.ts.
packages/capi-client/tsconfig.json Adds strict TS config for the new package.
packages/capi-client/src/index.ts Implements createApiClient wrapper around generated client/sdk.
packages/capi-client/src/index.test.ts Adds MSW/OpenAPI-based tests for stories.get and stories.getAll.
packages/capi-client/package.json Defines package metadata, scripts, deps, and Nx targets.
packages/capi-client/openapi-ts.config.ts Configures @hey-api/openapi-ts generation from bundled spec.
packages/capi-client/eslint.config.js Adds package-local ESLint configuration + ignores.
packages/capi-client/README.md Documents installation, usage, configuration, and support.
packages/capi-client/.npmignore Restricts published files to dist/.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- run: |
PATHS=$(pnpm list --recursive --depth=0 --json | jq -r '.[] | select(.private == false) | .path' | tr '\n' ' ')
pnpx pkg-pr-new publish $PATHS --compact --pnpm
pnpx pkg-pr-new publish $PATHS --pnpm
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This led to CI failing because the package does not exist (yet) on npm. We might revert this when we've created the package on npm.

Copy link
Contributor

Choose a reason for hiding this comment

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

@maoberlehner actually, during the work on richtext PR I realized it might not be necessary

image

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 24, 2026

Open in StackBlitz

@storyblok/astro

npm i https://pkg.pr.new/storyblok/monoblok/@storyblok/astro@428

@storyblok/api-client

npm i https://pkg.pr.new/storyblok/monoblok/@storyblok/api-client@428

storyblok

npm i https://pkg.pr.new/storyblok/monoblok/storyblok@428

@storyblok/eslint-config

npm i https://pkg.pr.new/storyblok/monoblok/@storyblok/eslint-config@428

@storyblok/js

npm i https://pkg.pr.new/storyblok/monoblok/@storyblok/js@428

storyblok-js-client

npm i https://pkg.pr.new/storyblok/monoblok/storyblok-js-client@428

@storyblok/management-api-client

npm i https://pkg.pr.new/storyblok/monoblok/@storyblok/management-api-client@428

@storyblok/nuxt

npm i https://pkg.pr.new/storyblok/monoblok/@storyblok/nuxt@428

@storyblok/react

npm i https://pkg.pr.new/storyblok/monoblok/@storyblok/react@428

@storyblok/region-helper

npm i https://pkg.pr.new/storyblok/monoblok/@storyblok/region-helper@428

@storyblok/richtext

npm i https://pkg.pr.new/storyblok/monoblok/@storyblok/richtext@428

@storyblok/svelte

npm i https://pkg.pr.new/storyblok/monoblok/@storyblok/svelte@428

@storyblok/vue

npm i https://pkg.pr.new/storyblok/monoblok/@storyblok/vue@428

commit: ce5eab3

@cursor
Copy link

cursor bot commented Feb 24, 2026

You have run out of free Bugbot PR reviews for this billing cycle. This will reset on March 1.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@dipankarmaikap
Copy link
Contributor

@maoberlehner I left a few small comments, but otherwise this looks good to be merged. Great work!

Introduce a new `@storyblok/api-client` package that provides a modern,
typed TS client for the Content Delivery (CAPI) stories endpoints,
generated from our OpenAPI spec. This improves DX and consistency when
consuming the Stories API.

- Add OpenAPI CAPI stories spec and shared schemas for story field types
- Generate typed CAPI client (fetch-based) and SDK with `get` and `getAll` helpers
- Implement `createApiClient` wrapper with region-aware base URL and access token handling
- Add basic tests using MSW + OpenAPI mocks to validate `stories.get` and `stories.getAll`
- Configure build (tsdown), linting, testing, and npm packaging for the new package

Fixes WDX-281
Switch to the ky-based client plugin and configure automatic retries
(including 429s) with backoff to make the CAPI client more resilient to
transient failures. Extend tests to cover retry behavior and error
handling with and without `throwOnError`, and ignore generated sources
in git.
The API accepts both numeric IDs and string identifiers; update the
OpenAPI schema so clients generate correct types and validation matches
actual behavior
Use fileURLToPath with import.meta.url instead of __dirname so tests can
resolve the OpenAPI spec path correctly in an ES module environment
Align GitHub branding across package READMEs to improve documentation
consistency and professionalism
Fix typo and standardize "GitHub repo" phrasing to improve clarity for
users creating minimal reproducible examples when they can't share
company code
Use local CONTRIBUTING.md and correct package naming so contributors see
repo-specific guidelines instead of central .github docs
Align CAPI client and OpenAPI spec with personal access token security
by moving authentication from query parameters to the client auth
configuration, avoiding token leakage in URLs and keeping docs
consistent.
- Remove unused input type schemas that were never referenced by MAPI or CAPI
- Delete story-content-input and all field-type-input variants (231 lines)
- Remove duplicate components.yaml (BearerAuth already in security-schemes)
- Fix China region server URL (storyblok.cn → storyblokchina.cn)
- Normalize indentation in shared responses.yaml
Ensure story schema required fields and properties reflect actual
Storyblok responses and metadata, and document pagination headers on the
stories list endpoint to keep API docs accurate and complete
Introduce reusable cache strategy handlers (cache-first, network-first,
swr) so cache behavior is explicit, easier to reason about, and
override. Simplify SWR semantics to always return cached data when
present and revalidate in the background with per-key deduplication, and
improve in-memory cache defaults (implicit storedAt, documented
eviction).
Add optional relation inlining for stories.get and stories.getAll.

Also fetch missing rel_uuids automatically and resolve relation cycles safely.
Remove --compact from preview publishing so CI does not require packages to be pre-published on npm.
Align tests and utilities with the new stories client generation path
after directory restructuring.
The documented behavior for handling relation UUIDs no longer matches
the current client implementation, so removing it prevents misleading
usage assumptions and keeps the README accurate.
Prepare @storyblok/api-client for a 0.1.0 release so it can be published
and consumed as a publicly usable package
Copy link
Contributor

@alexjoverm alexjoverm left a comment

Choose a reason for hiding this comment

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

@maoberlehner this is a solid improvement over current one. Well structured, with cache strategies, and the full jitter you already established in the mapi client, just to highlight a few niceties.

There are a few things that require further thought and polishing, mostly around the cv, cache and concurrency areas.

Comment on lines +6 to +7
const UUID_CHUNK_SIZE = 50;
const MAX_CONCURRENT_REQUESTS = 50;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion(non-blocking): shall we have these in a separate constants file?

rawQuery: Record<string, unknown>,
fetchFn: (query: Record<string, unknown>) => Promise<ApiResponse<TData>>,
): Promise<ApiResponse<TData>> => {
const query = currentCv ? applyCvToQuery(rawQuery, currentCv) : rawQuery;
Copy link
Contributor

Choose a reason for hiding this comment

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

(blocking): heads up here - cv can be 0 (actually, it's a common pattern to bypass CDN cache so this would fail in that case. Better currentCV !== undefined

throwOnError?: ThrowOnError;
cache?: CacheConfig;
inlineRelations?: InlineRelations;
retry?: RetryOptions;
Copy link
Contributor

Choose a reason for hiding this comment

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

(blocking): @maoberlehner is possible the client doesn't have any rate limit/throttling logic ATM? It's important as, for at-scale scenarios, like SSG or high peaks in SSR, we need to provide the user both a reactive (retry mechanism) and a preventive (rate limits) they can use to avoid extra costs and proper management they way each needs.

What do you think about option for same strategy we have in current js-client? (providing tiers as default, but overrideable by user)

@@ -0,0 +1,137 @@
export type CacheStrategy = 'cache-first' | 'network-first' | 'swr';
Copy link
Contributor

Choose a reason for hiding this comment

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

praise: love the strategies! gives mature default behaviours

@@ -0,0 +1,492 @@
import { createClient, createConfig } from './generated/stories/client';
Copy link
Contributor

Choose a reason for hiding this comment

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

(blocking) @maoberlehner correct me if I'm wrong, but ATM the flushing behavior is auto and the user cannot opt out from it, correct? That can be problematic as, at-scale scenarios, often users want to have external control of the CV and the flush behavior.

For example:

  1. Often they have a CV stored in a distributed Redis, with multiple load-balanced API's (thus multiple capi-client instances). the capi client shouldn't either auto-flush, rather user need to have manual control of flushing and cv management
  2. Flushing immediately after a webhook is triggered (story published, deleted, updated, etc)
  3. Rare, but imagine that during a SSG build, a content editor publishes a set of stories. Likely part of the generated website would have version A, and other part version B
    etc etc

In current js-client, that's customizable via an option, say like:

cache: {
  flush: 'auto' | 'manual'
}

Of course, the manual would need some kind of client.flushCache() so users can handle it themselves.

@@ -0,0 +1,584 @@
# Storyblok Content Delivery API Client
Copy link
Contributor

Choose a reason for hiding this comment

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

(blocking): we need to keep the usual Readme template, and instead move this content into a pkg ref in the Docs platform

summary: Retrieve Multiple Datasources
description: Returns an array of all datasources with pagination support.
parameters:
- name: page
Copy link
Contributor

Choose a reason for hiding this comment

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

@maoberlehner I think we're missing the cv param, both in /datasources and /datasources/{id} requests (they're in the response already)

return networkResult;
}

const key = createCacheKey(method, path, rawQuery);
Copy link
Contributor

@alexjoverm alexjoverm Feb 27, 2026

Choose a reason for hiding this comment

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

@maoberlehner do you think we need to include cv (the full query) in the cache key? How would it behave if we're querying /story-a?version=published first with cv=1 and then gets revalidated and we request with cv=2? As CV is out of the key, would it still get the one with cv=1?

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.

4 participants