@kywi-software/sdk
v0.24.0
Published
Standalone HTTP client for the Kywi CMS REST API — headless/decoupled frontends, no dependency on @kywi-software/core.
Maintainers
Readme
@kywi-software/sdk
A standalone HTTP client for the Kywi CMS REST API (/api/v1/*). Pure fetch
under the hood, no runtime dependency on @kywi-software/core — usable from a headless
frontend, a separate Node service, or any React app under ./hooks.
Every export below is read from packages/sdk/src/*.ts and its tests
(packages/sdk/src/__tests__/*.test.ts); if this file and the source ever
disagree, the source wins.
Install
npm install @kywi-software/sdkInside this monorepo the package is linked via the workspace protocol.
pnpm --filter @kywi-software/sdk build compiles src/ to dist/ via
tsc; exports in package.json points . at dist/index.js and
./hooks at dist/hooks/index.js. react/react-dom are optional peer
dependencies, only needed for ./hooks.
Quick start
import { createKywiSdk } from '@kywi-software/sdk'
const kywi = createKywiSdk({
baseUrl: 'https://example.com/api/v1',
// one of:
apiKey: 'kywi_xxxxxxxx', // a Kywi API key (from /admin/web-services/api-keys)
// token: '<jwt>', // or a JWT access token (e.g. from kywi.auth.login())
defaultSiteId: 'default', // used when a call omits siteId
})
const page = await kywi.content.getBySlug('about')
const nav = await kywi.nav.getTree()Authentication — how the API key / token actually rides on the request
KywiHttpClient (src/http-client.ts) puts both apiKey and token on
the Authorization: Bearer <value> header — there is no x-api-key header.
This matches the server: resolveAuth reads Authorization: Bearer <token>
and treats a value with the kywi_ prefix as an API key, anything else as a
JWT. If both apiKey and token are set, token wins (it's applied last in
the constructor).
// API key
createKywiSdk({ baseUrl, apiKey: 'kywi_xxxxxxxx' })
// JWT (e.g. after auth.login())
const { accessToken } = await kywi.auth.login('[email protected]', 'password')
createKywiSdk({ baseUrl, token: accessToken })auth.refresh() and manual token rotation update the client via
http.setToken(newToken) (not exposed on the scope object directly — call it
on a KywiHttpClient instance you construct yourself if you need to swap
tokens on a long-lived client without rebuilding the whole SDK).
Every success response unwraps { data }
The Kywi API wraps every success body in { data: … } (list endpoints add
{ data, meta }) and every error in { error: { code, message, details? } }
(jsonOk/jsonError server-side). Every SDK client — including
auth, added after the original scopes — unwraps res.data so callers get
plain values (ContentItem, AuthResult, etc.), never the envelope. A 404 is
returned as the parsed { error } body rather than thrown, which most clients
turn into null (e.g. content.getById, content.getBySlug); a non-OK,
non-404 response throws KywiSdkError with .status and .body.
siteId goes on the query string, not the request body
content.create() resolves siteId (explicit arg, else config.defaultSiteId)
and appends it as a query param — POST /content/page?siteId=default — not as
a body field. The server's create schema is .strict(), so a siteId key
inside the JSON body is rejected with 400. Every read/list method
(getBySlug, list, …) also sends siteId as a query param, for consistency.
// Correct — siteId is threaded through as a query param internally
await kywi.content.create('page', { title: 'New Page' }, 'site-b-corp')
// Do NOT do this — { siteId } inside `data` would be a plain field write,
// and the server itself would 400 on an unknown strict-schema key.Scopes
createKywiSdk() returns a KywiSdkScope (src/types.ts) with these
sub-scopes, all HTTP-backed except i18n (client-side only, from
pre-loaded bundles):
| Scope | Methods | Notes |
|---|---|---|
| auth | login, register, refresh, forgotPassword, resetPassword, logout | src/auth-client.ts |
| content | getBySlug, getById, getChildren, list, create, update, delete, publish, unpublish | type-agnostic ops (getById/delete/publish/unpublish/getChildren) hit /content/by-id/:id*; typed ops (list/create/update) hit /content/:type* |
| nav | getTree, getBreadcrumbs, getChildren | |
| site | getConfig, list, updateSettings | |
| user | getCurrent, getById, list, create | |
| feeds | query, get, list, create, update, delete | |
| media | upload, list, getById, delete | upload takes a File \| Buffer |
| layout | get, update, updateRegion | page layout documents |
| search | search | |
| versions | list, get, restore | content version history |
| workflow | publish, unpublish, restore, permanentDelete | trash/publish workflow, ID-based |
| categories | list, getById, create, update, delete, tree | |
| forms | submit, listSubmissions, getCsrfToken | |
| tree | getChildren, getBreadcrumbs, move, reorder | site-tree drag/drop operations |
| hooks | listWebhooks, createWebhook, updateWebhook, deleteWebhook | webhook endpoints |
| i18n (optional) | get, getLocale | only present when config.bundles and config.locale are both set |
The full method signatures are the source of truth in src/types.ts
(KywiSdk*Scope interfaces) — read them before wiring a new call, since
several return null on 404 rather than throwing (content.getById,
content.getBySlug, content.update, site.getConfig, user.getCurrent,
user.getById) while others throw KywiSdkError.
React hooks (@kywi-software/sdk/hooks)
import { KywiSdkProvider, useKywiSdkContent, useKywiSdkNav } from '@kywi-software/sdk/hooks'
import { createKywiSdk } from '@kywi-software/sdk'
const sdk = createKywiSdk({ baseUrl: '/api/v1', defaultSiteId: 'default' })
function App() {
return (
<KywiSdkProvider sdk={sdk}>
<Page />
</KywiSdkProvider>
)
}
function Page() {
const { data, loading, error } = useKywiSdkContent('about')
const nav = useKywiSdkNav()
// ...
}Exported from src/hooks/index.ts: KywiSdkProvider/useKywiSdk (context),
useSdkQuery (generic fetch-on-mount-and-deps-change hook), plus thin
wrappers useKywiSdkContent/useKywiSdkContentById/useKywiSdkContentList,
useKywiSdkNav/useKywiSdkBreadcrumbs, useKywiSdkFeed/useKywiSdkFeedQuery.
react/react-dom are optional peer dependencies — only needed if you import
from @kywi-software/sdk/hooks.
Errors
import { KywiSdkError } from '@kywi-software/sdk'
try {
await kywi.content.create('page', { title: 'x' })
} catch (err) {
if (err instanceof KywiSdkError) {
console.error(err.status, err.body)
}
}Testing
pnpm --filter @kywi-software/sdk testAll client tests stub global.fetch (vi.stubGlobal('fetch', mockFetch)) —
no network or database access required.
