Skip to main content
Version: 3.x

Fetch Client

Available since 3.7.0
Preview Feature
Fetch Client is in preview and may be subject to breaking changes in future releases.

Overview

@zenstackhq/fetch-client is a lightweight, framework-agnostic client for consuming ZenStack's RPC-style auto CRUD API. It exposes a fully typed object that mirrors the ORM's ZenStackClient API, but every call is dispatched as an HTTP request via the global fetch (or a custom one you provide).

Use it when you want strongly typed CRUD access from any JavaScript runtime — a non-React/Vue/Svelte UI, a Node.js script, a worker, etc. — without pulling in a data-fetching library.

info

The fetch client only works with the RPC-style API.

Installation

npm install @zenstackhq/fetch-client

Creating a Client

Call createClient with your schema and an endpoint:

import { createClient } from '@zenstackhq/fetch-client';
import { schema } from '~/zenstack/schema-lite';

const client = createClient(schema, {
endpoint: 'https://example.com/api/model',
});

const users = await client.user.findMany({ include: { posts: true } });
const post = await client.post.create({ data: { title: 'Hello' } });

Like with the TanStack Query integration, you can pass the --lite flag to zen generate to produce a schema-lite.ts that strips sensitive content (e.g., access policies) from the schema object, so it's safe to import into client-side code. See the CLI Reference for details.

Options

  • endpoint (required)

    The base URL of the RPC API. Must be a fully qualified URL (e.g., https://example.com/api/model).

  • fetch

    A custom fetch function to use instead of the global fetch. Useful for attaching authentication headers, instrumenting requests, or routing through a proxy.

    import { createClient, type FetchFn } from '@zenstackhq/fetch-client';

    const myFetch: FetchFn = (url, init) => {
    const headers = { ...init?.headers, authorization: `Bearer ${getToken()}` };
    return fetch(url, { ...init, headers });
    };

    const client = createClient(schema, {
    endpoint: 'https://example.com/api/model',
    fetch: myFetch,
    });

Custom Procedures

If your schema defines custom procedures, they are exposed under the $procs accessor. Query procedures get a query method (issues a GET); mutation procedures get a mutate method (issues a POST):

// Query procedure
const stats = await client.$procs.getStats.query();

// Mutation procedure
await client.$procs.sendNotification.mutate({ args: { message: 'hello' } });

Sequential Transactions

$transaction lets you execute multiple operations atomically in a single request. It mirrors the sequential transaction overload on the ORM — all operations are sent together and executed in order; if any fails, the whole transaction is rolled back.

Each operation is { model, op, args }, where model is the PascalCase model name, op is the operation name (create, findMany, etc.), and args matches the corresponding ORM call. args can be omitted for operations whose args are entirely optional (e.g., findMany, count, deleteMany).

const [user, post] = await client.$transaction([
{ model: 'User', op: 'create', args: { data: { email: 'alice@example.com' } } },
{ model: 'Post', op: 'create', args: { data: { title: 'Hello' } } },
]);

The result tuple is typed per-position based on each operation's return type:

const results = await client.$transaction([
{ model: 'User', op: 'create', args: { data: { email: 'a@b.com' } } },
{ model: 'User', op: 'findMany' },
{ model: 'User', op: 'count' },
]);

results[0].email; // User
results[1][0]?.id; // User[]
results[2]; // number
info

Only sequential transactions are supported on the client. Interactive transactions are intentionally not exposed, since holding a database transaction open across multiple network round-trips would be harmful to server scalability.

Respecting ORM Client Customization

By default, createClient(schema, ...) gives you typed accessors for all models and all CRUD operations, with result types derived from the schema. If your server-side ORM client is customized — with slicing, field omission, or plugin-contributed result fields — the fetch client's types won't automatically reflect those customizations.

To keep them in sync, pass the client type as a generic parameter:

import { createClient } from '@zenstackhq/fetch-client';
import { schema } from '~/zenstack/schema-lite';

// Type-only import of your server-side client
import type { DbType } from '~/server/db';

const client = createClient<DbType>(schema, {
endpoint: 'https://example.com/api/model',
});

With the generic in place, slicing trims unavailable models and operations from the client's type, omitted fields are removed from result types, and plugin-contributed fields appear in result types. The runtime behavior itself happens on the server — the generic is purely for type alignment.

Comments
Feel free to ask questions, give feedback, or report issues.

Don't Spam


You can edit/delete your comments by going directly to the discussion, clicking on the 'comments' link below