feat(capi-client): add typed Content Delivery API client package#428
feat(capi-client): add typed Content Delivery API client package#428maoberlehner wants to merge 26 commits intomainfrom
Conversation
4945d5f to
093cf73
Compare
There was a problem hiding this comment.
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.
packages/openapi/resources/shared/stories/story-content-input.yaml
Outdated
Show resolved
Hide resolved
| - 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@maoberlehner actually, during the work on richtext PR I realized it might not be necessary
@storyblok/astro
@storyblok/api-client
storyblok
@storyblok/eslint-config
@storyblok/js
storyblok-js-client
@storyblok/management-api-client
@storyblok/nuxt
@storyblok/react
@storyblok/region-helper
@storyblok/richtext
@storyblok/svelte
@storyblok/vue
commit: |
|
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. |
|
@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
d162096 to
ce5eab3
Compare
alexjoverm
left a comment
There was a problem hiding this comment.
@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.
| const UUID_CHUNK_SIZE = 50; | ||
| const MAX_CONCURRENT_REQUESTS = 50; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
(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; |
There was a problem hiding this comment.
(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'; | |||
There was a problem hiding this comment.
praise: love the strategies! gives mature default behaviours
| @@ -0,0 +1,492 @@ | |||
| import { createClient, createConfig } from './generated/stories/client'; | |||
There was a problem hiding this comment.
(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:
- 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
- Flushing immediately after a webhook is triggered (story published, deleted, updated, etc)
- 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 | |||
There was a problem hiding this comment.
(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 |
There was a problem hiding this comment.
@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); |
There was a problem hiding this comment.
@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?
Introduce a new
@storyblok/api-clientpackage 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.getandgetAllhelperscreateApiClientwrapper with region-aware base URL and access token handlingstories.getandstories.getAllFixes WDX-281