AkurAI Build
Menu

Public repository / master

AkurAI-Framework / site/content/docs/client-sdk.md

Latest change 1d2bfb32048e3c8b890ef369116a9fa65bac00ec - Add typed client SDK (akurai-client.js), demo page, and docs by Ólafur Búi Ólafsson

# Typed client SDK

The [auto-API](/docs/auto-api) gives you a full REST surface for every
collection you declare. The client SDK — `akurai-client.js` — wraps that surface
in a small, typed JavaScript client so you call `client.posts.create(...)`
instead of hand-rolling `fetch` URLs, methods, and error handling. It's the
framework's "tRPC analog": one generated client whose shape mirrors your data
model.

True to the rest of the stack, **there is no build step**. `akurai-client.js` is
a native ES module the browser loads directly. "Typed" means thorough JSDoc
`@typedef` + `@param`/`@returns` annotations — editors and TypeScript-in-checkJS
read them for completions and type-checking, with nothing to compile.

See it live on the [client SDK demo](/client-demo).

## Connecting

The client is generated at runtime from the manifest at `/api/_meta`, so it
always matches the collections your server actually declared. Because it must
fetch that manifest first, `createClient` is async:

```js
import { createClient } from "/akurai-client.js";

const client = await createClient();   // same-origin; awaits GET /api/_meta
```

Pass a base URL to target another origin:

```js
const client = await createClient("https://example.com");
```

On connect it logs a breadcrumb:

```
[client] connected, 1 collection(s): posts
```

The raw manifest and the discovered collection names stay on the client:

```js
client.meta;             // the full /api/_meta document
client.collections;      // the `collections` array from the manifest
client.collectionNames;  // ["posts", ...]
```

## Per-collection methods

For every collection in the manifest, the client gets an accessor keyed by the
collection's name (`client.posts`, `client.<yourCollection>`, …). Each accessor
has six methods:

| Method | Calls | Returns |
| ------ | ----- | ------- |
| `list(opts?)` | `GET /api/collections/<name>/records` | `AkuraiRecord[]` (newest first) |
| `create(data)` | `POST .../records` | the created `AkuraiRecord` |
| `get(id)` | `GET .../records/<id>` | one `AkuraiRecord` |
| `update(id, patch)` | `PATCH .../records/<id>` | the updated `AkuraiRecord` |
| `delete(id)` | `DELETE .../records/<id>` | `void` (resolves on 204) |
| `search(query, opts?)` | `GET .../records?search=<query>` | `AkuraiRecord[]` |

`opts` accepts `{ limit }`, which becomes `?limit=` on `list` and `search`.

```js
const posts = await client.posts.list({ limit: 20 });
const made  = await client.posts.create({ title: "Hello", body: "a world" });
const one   = await client.posts.get(made.id);
await client.posts.update(made.id, { title: "Hello again" });
const hits  = await client.posts.search("world", { limit: 5 });
await client.posts.delete(made.id);
```

## Errors

Any non-2xx response throws an `AkuraiError` carrying the HTTP `status`, the
request `url`, and the server's `{ error }` message (the auto-API returns that
on `400`/`404`):

```js
import { AkuraiError } from "/akurai-client.js";

try {
  await client.posts.create({}); // missing a required field → 400
} catch (err) {
  if (err instanceof AkuraiError) {
    console.error(err.status, err.message); // 400, "AkurAI API 400 on … : title is required"
  }
}
```

## The typing approach (no build step)

Types are expressed entirely in JSDoc, so editors give you completions and
inline type-checking with nothing to compile:

- `AkuraiRecord` — a stored record (`id`, `created`, plus your declared fields).
- `AkuraiField` / `AkuraiCollectionMeta` — the manifest's schema shape.
- `AkuraiCollectionClient` — the six-method bundle each `client.<name>` exposes.
- `AkuraiClient` — the connected client (`meta`, `collections`,
  `collectionNames`, `baseUrl`, plus the dynamic per-collection accessors).

Add `// @ts-check` at the top of a consuming module (or enable `checkJs` in your
editor) and you get type errors for things like passing a number where a record
body is expected — without ever leaving plain `.js`.

Because accessor keys (`posts`, …) are generated from your manifest, they can't
be statically enumerated in the typedef; enumerate them at runtime with
`client.collectionNames`.

## Verifying it works

The live site ships a demo `posts` collection. Open
[`/client-demo`](/client-demo) and:

1. **Console** shows `[client] connected, N collection(s): posts`.
2. The **Posts** panel lists existing records via `client.posts.list()`.
3. Typing a title and pressing **Create** calls `client.posts.create()`, then
   re-lists — the new post appears at the top.
4. Submitting with validation issues surfaces the server's `{ error }` message
   inline (thrown as an `AkuraiError`).