# Zucms

## Typescript SDK

> Category: API

---

## Pages

- [Introduction](https://docs.zucms.co/introduction)

### API

- [RESTful API](https://docs.zucms.co/api/rest)
- [Typescript SDK](https://docs.zucms.co/api/typescript)

---

# Typescript SDK

## Installation

Let's get started by installing the SDK.

```bash
pnpm install zucms
```

## Overview

This SDK is built around one idea:

- The client stays unopinionated at runtime.
- You add types exactly where you need them with `<T>`.
- Without an explicit generic, response data stays `unknown`.

That keeps the API small and predictable while still allowing full typing for CMS models, localized fields, create payloads, and update payloads.

## Quick Start

```ts name="main.ts"
import { createClient, type Localized } from 'zucms';

type Article = {
	status: 'draft' | 'published';
	title: Localized<string, 'de' | 'en'>;
};

const client = createClient({
	baseUrl: 'https://api.example.com',
	apiKey: 'zw_...',
});

const articles = await client.model('articles').list<Article>();
```

## Create a Client

```ts name="zucms.ts"
import { createClient } from 'zucms';

const client = createClient({
	baseUrl: 'https://api.example.com',
	apiKey: 'zw_...',
});
```

### Config

```ts name="config.ts"
type CreateClientConfig = {
	apiKey: string;
	authMode?: 'bearer' | 'x-api-key';
	baseUrl: string;
	fetch?: typeof globalThis.fetch;
	headers?: HeadersInit;
};
```

Notes:

- `authMode` defaults to `'bearer'`.
- `baseUrl` may include or omit a trailing slash.
- `fetch` can be overridden for tests or custom runtimes.
- `headers` are merged into every request.

## Typing Model Data

The SDK does not require a global schema map.

You type each request explicitly:

```ts name="types.ts"
type Article = {
	status: 'draft' | 'published';
	title: string;
};

const list = await client.model('articles').list<Article>();
const entry = await client.get<Article>('articles', 'entry_1');
```

If you skip the generic, the returned entry data is `unknown`:

```ts name="action.ts"
const list = await client.model('articles').list();
// list.data[number].data is unknown
```

### Different Types for Write and Read

If your create payload differs from the response shape, use two generics:

```ts name="types.ts"
type CreateArticle = {
	status: 'draft' | 'published';
	title: string;
};

type Article = CreateArticle & {
	seoSlug: string;
};

const created = await client.create<CreateArticle, Article>('articles', {
	status: 'draft',
	title: 'Hello',
});
```

The same pattern works for updates:

```ts name="types.ts"
type UpdateArticle = {
	status?: 'draft' | 'published';
	title?: string;
};

const updated = await client.update<UpdateArticle, Article>('articles', 'entry_1', {
	title: 'Updated',
});
```

## Localized Fields

Use `Localized<TValue, TLocale>` for localized CMS fields:

```ts name="types.ts"
import type { Localized } from 'zucms';

type Article = {
	title: Localized<string, 'de' | 'en'>;
	description: Localized<string, 'de' | 'en'>;
};
```

This also works for nested objects:

```ts name="types.ts"
type SeoFields = {
	description: Localized<string, 'de' | 'en'>;
	title: Localized<string, 'de' | 'en'>;
};
```

## API Shapes

You can use the client in two styles:

- `client.model(modelKey)` for model-scoped calls
- top-level `client.list/get/create/update/delete` if you prefer passing the model key every time

### Model-Scoped API

```ts name="actions.ts"
const articles = client.model('articles');

await articles.list<Article>();
await articles.get<Article>('entry_1');
await articles.create<Article>({
	status: 'draft',
	title: 'Hello',
});
await articles.update<Partial<Article>>('entry_1', {
	status: 'published',
});
await articles.delete('entry_1');
```

### Top-Level API

```ts name="actions.ts"
await client.list<Article>('articles');
await client.get<Article>('articles', 'entry_1');
await client.create<{ title: string }, Article>('articles', { title: 'Hello' });
await client.update<Partial<Article>, Article>('articles', 'entry_1', {
	status: 'published',
});
await client.delete('articles', 'entry_1');
```

## Queries

### List Entries

```ts name="actions.ts"
const response = await client.model('articles').list<Article>({
	page: 1,
	pageSize: 25,
	search: 'hello',
	sort: ['title', '-createdAt'],
	fields: ['title', 'status'],
	include: ['author'],
	locale: 'de',
	fallback: true,
	filter: {
		status: { eq: 'published' },
		title: { contains: 'hello' },
	},
});
```

Supported query fields:

- `page`
- `pageSize`
- `search`
- `sort`
- `fields`
- `include`
- `locale`
- `fallback`
- `filter`

The SDK serializes filters to the LWAPI deep-object format:

```txt
filter[status][eq]=published
filter[title][contains]=hello
```

### Get Entry

```ts name="actions.ts"
const article = await client.model('articles').get<Article>('entry_1', {
	locale: 'en',
	fallback: true,
});
```

## Responses

### List Response

```ts name="actions.ts"
type SdkListResponse<TData> = {
	data: Array<{
		createdAt: string;
		data: TData;
		id: string;
		modelKey: string;
		tenantId: string;
		updatedAt: string;
	}>;
	meta: {
		fields: string[];
		filters: Record<string, unknown>;
		include: string[];
		modelKey: string;
		page: number;
		pageSize: number;
		search: string | null;
		sort: string[];
		tenantId: string;
		total: number;
		totalPages: number;
	};
};
```

### Single Response

```ts name="types.ts"
type SdkSingleResponse<TData> = {
	data: {
		createdAt: string;
		data: TData;
		id: string;
		modelKey: string;
		tenantId: string;
		updatedAt: string;
	};
};
```

### Delete Response

```ts name="types.ts"
type SdkDeleteResponse = {
	data: {
		deleted: boolean;
		id: string;
	};
};
```

## Error Handling

Non-2xx responses throw `SdkClientError`.

```ts name="actions.ts"
import { SdkClientError } from 'zucms';

try {
	await client.model('articles').get<Article>('missing');
} catch (error) {
	if (error instanceof SdkClientError) {
		console.log(error.status);
		console.log(error.code);
		console.log(error.message);
		console.log(error.details);
		console.log(error.payload);
	}
}
```

Available fields:

- `status`
- `code`
- `message`
- `details`
- `payload`

## Auth Modes

### Bearer Token

```ts name="zucms.ts"
const client = createClient({
	baseUrl: 'https://api.example.com',
	apiKey: 'zw_...',
	authMode: 'bearer',
});
```

Sends:

```txt
Authorization: Bearer zw_...
```

### API Key Header

```ts
const client = createClient({
	baseUrl: 'https://api.example.com',
	apiKey: 'zw_...',
	authMode: 'x-api-key',
});
```

Sends:

```txt
x-api-key: zw_...
```

## Custom Headers and Abort Signals

```ts name="actions.ts"
const controller = new AbortController();

await client.model('articles').list<Article>(
	{ page: 1 },
	{
		signal: controller.signal,
		headers: {
			'x-request-id': 'req_123',
		},
	},
);
```

## Current Endpoint Coverage

The SDK currently targets these LWAPI endpoints:

- `GET /api/models/{modelKey}/entries`
- `POST /api/models/{modelKey}/entries`
- `GET /api/models/{modelKey}/entries/{entryId}`
- `PATCH /api/models/{modelKey}/entries/{entryId}`
- `DELETE /api/models/{modelKey}/entries/{entryId}`

## Exports

Main exports:

- `createClient`
- `SdkClientError`
- `Localized`
- `SdkListQuery`
- `SdkGetQuery`
- `SdkEntry`
- `SdkListResponse`
- `SdkSingleResponse`
- `SdkDeleteResponse`
- `SdkErrorResponse`

## Example End to End

```ts name="actions.ts"
import {
	createClient,
	type Localized,
	type SdkSingleResponse,
} from 'zucms';

type Article = {
	status: 'draft' | 'published';
	title: Localized<string, 'de' | 'en'>;
};

type CreateArticle = {
	status: 'draft' | 'published';
	title: Localized<string, 'de' | 'en'>;
};

const client = createClient({
	baseUrl: 'https://api.example.com',
	apiKey: 'zw_...',
});

const created: SdkSingleResponse<Article> = await client.model('articles').create<
	CreateArticle,
	Article
>({
	status: 'draft',
	title: {
		de: 'Hallo Welt',
		en: 'Hello World',
	},
});

const articles = await client.model('articles').list<Article>({
	locale: 'de',
	fallback: true,
	sort: ['-createdAt'],
});
```

