agentsclimarketplace

Zenstack crud server

Skill zenstackhq/skills/skills/zenstack-crud-server

Expose ZenStack V3 data as automatic CRUD web APIs ("Query as a Service") and consume them. Use when adding REST/RPC endpoints with a server adapter (Express, Fastify, Next.js, Nuxt, SvelteKit, Hono, Elysia, TanStack Start), wiring per-request access control via getClient/$setAuth, generating an OpenAPI spec, or calling the API from a client (fetch-client, TanStack Query hooks).From its SKILL.md

Install
npx -y skills add zenstackhq/skills --skill zenstack-crud-server

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
  • runs commandsInstructs the agent to run 1 command, including `npm install @zenstackhq/server`.

SKILL.md

11.5 KB, ~2.9k tokens by cl100k_base, as published. Nobody here has run it

ZenStack V3 — Automatic CRUD Services (Query as a Service)

ZenStack turns your data model into secure CRUD web APIs with no hand-written controllers. Two pieces combine:

  • API handler — framework-agnostic; defines the API style (RPC or RESTful) and translates HTTP requests into ORM queries. From @zenstackhq/server/api.
  • Server adapter — framework-specific glue that installs a handler into Express/Next.js/etc. and supplies a per-request, policy-enforced ORM client. From @zenstackhq/server/<framework>.
HTTP request → server adapter → API handler (RPC | REST) → ZenStack ORM (policies applied) → DB

This builds on the other skills: define the schema with zenstack-schema-modeling, secure it with zenstack-access-control, and create the base client with zenstack-querying. Install the server package: npm install @zenstackhq/server.

API handlers

RPC — mirrors the ORM API 1:1. Routes look like GET /post/findMany?q=<urlencoded-json> and POST /post/create (body { data: ... }); responses are wrapped in { data: ... }.

import { RPCApiHandler } from '@zenstackhq/server/api';
import { schema } from '~/zenstack/schema';

const apiHandler = new RPCApiHandler({ schema });

RPC endpoints: every ORM op (findMany, findUnique, findFirst, count, aggregate, groupBy, create, createMany, createManyAndReturn, upsert, update, updateMany, updateManyAndReturn, delete, deleteMany), plus $procs/<name> for custom procedures and $transaction/sequential. Status codes: 201 create, 200 other success, 400 malformed, 403 policy violation, 404 not found, 422 validation error, 500 unexpected.

RESTful — JSON:API v1.1 compliant. Routes like GET /post, GET /post/:id, POST /post, PUT|PATCH /post/:id, DELETE /post/:id, plus relationship routes.

import { RestApiHandler } from '@zenstackhq/server/api'; // note: RestApiHandler, not RESTful

const apiHandler = new RestApiHandler({
    schema,
    endpoint: 'http://localhost:3000/api', // required — used to build resource links
});

RestApiHandler options: endpoint (required), pageSize (default 100; Infinity disables paging), modelNameMapping ({ User: 'users' }), externalIdMapping ({ Tag: 'name' }), nestedRoutes (bool). REST query params: filter[field]=, filter[field$op]= ($lt,$gt, $contains,$startsWith,…), sort=field,-other, page[offset]=/page[limit]=, include=rel,rel.nested, fields[type]=a,b.

Both handlers also accept queryOptions for slicing/omitting what the API exposes:

new RPCApiHandler({
    schema,
    queryOptions: {
        slicing: {
            includedModels: ['User', 'Post'],
            models: { post: { excludedOperations: ['delete'] } },
        },
        omit: { user: { password: true } },
    },
});

Server adapters — getClient is where access control lives

Every adapter takes apiHandler and a getClient(request) => ClientContract callback. Return a policy-enforced client bound to the current user via $setAuth (see zenstack-access-control), so each request is isolated:

getClient: (req) => authDb.$setAuth(getSessionUser(req)),

authDb is db.$use(new PolicyPlugin()). Return authDb.$setAuth(undefined) for anonymous, or the raw db to bypass policies (rarely what you want for a public API).

Express

import { ZenStackMiddleware } from '@zenstackhq/server/express';

app.use(express.json());
app.use(
    '/api/model',
    ZenStackMiddleware({
        apiHandler,
        getClient: (req) => authDb.$setAuth(getSessionUser(req)),
    }),
);

(Optional sendResponse: false writes to res.locals and calls next() instead.)

Fastify

import { ZenStackFastifyPlugin } from '@zenstackhq/server/fastify';

server.register(ZenStackFastifyPlugin, {
    prefix: '/api/model', // required
    apiHandler,
    getClient: (req) => authDb.$setAuth(getSessionUser(req)),
});

Next.js — App Router

// src/app/api/model/[...path]/route.ts
import { NextRequestHandler } from '@zenstackhq/server/next';

const handler = NextRequestHandler({
    apiHandler,
    getClient: (req) => authDb.$setAuth(getSessionUser(req)),
    useAppDir: true,
});
export {
    handler as GET,
    handler as POST,
    handler as PUT,
    handler as PATCH,
    handler as DELETE,
};

Next.js — Pages Router

// src/pages/api/model/[...path].ts
import { NextRequestHandler } from '@zenstackhq/server/next';

export default NextRequestHandler({
    apiHandler,
    getClient: (req, res) => authDb.$setAuth(getSessionUser(req, res)),
});

Nuxt

// server/api/model/[...].ts
import { createEventHandler } from '@zenstackhq/server/nuxt';

export default createEventHandler({
    apiHandler,
    getClient: (event) => authDb.$setAuth(getSessionUser(event)),
});

SvelteKit (API route — preferred; wildcard param must be named path)

// src/routes/api/model/[...path]/+server.ts
import { SvelteKitRouteHandler } from '@zenstackhq/server/sveltekit';

const handler = SvelteKitRouteHandler({
    apiHandler,
    getClient: (event) => authDb.$setAuth(getSessionUser(event)),
});
export const GET = handler,
    POST = handler,
    PUT = handler,
    PATCH = handler,
    DELETE = handler;

(Legacy SvelteKitHandler in hooks.server.ts with a prefix option is deprecated.)

Hono

import { createHonoHandler } from '@zenstackhq/server/hono';

app.use(
    '/api/model/*',
    createHonoHandler({
        apiHandler,
        getClient: (ctx) => authDb.$setAuth(getSessionUser(ctx)),
    }),
);

Elysia

import { createElysiaHandler } from '@zenstackhq/server/elysia';

app.group('/crud', (app) =>
    app.use(
        createElysiaHandler({
            apiHandler,
            basePath: '/api/model',
            getClient: (ctx) => authDb.$setAuth(getSessionUser(ctx)),
        }),
    ),
);

TanStack Start

// app/routes/api/$.ts
import { TanStackStartHandler } from '@zenstackhq/server/tanstack-start';

const handler = TanStackStartHandler({
    apiHandler,
    getClient: (req) => authDb.$setAuth(getSessionUser(req)),
});
export const Route = createFileRoute('/api/$')({
    server: {
        handlers: {
            GET: handler,
            POST: handler,
            PUT: handler,
            PATCH: handler,
            DELETE: handler,
        },
    },
});

Data type serialization

Handlers use superjson so non-JSON types survive the wire: DateTime → ISO string, Bytes → base64, BigInt/Decimal → string. The generated client SDKs handle this automatically. If you hand-craft requests, include the superjson meta under a serialization key ({ "data": ..., "meta": { "serialization": <meta> } }); responses carry the same.

Consuming the API

Fetch client (v3.7.0+, RPC APIs only)

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

const client = createClient(schema, {
    endpoint: 'https://example.com/api/model',
});
const users = await client.user.findMany({ include: { posts: true } });
await client.post.create({ data: { title: 'Hello' } });

Add auth via a custom fetch:

import type { FetchFn } from '@zenstackhq/fetch-client';
const fetchFn: FetchFn = (url, init) =>
    fetch(url, {
        ...init,
        headers: { ...init?.headers, authorization: `Bearer ${getToken()}` },
    });
createClient(schema, { endpoint, fetch: fetchFn });

Custom procedures: client.$procs.getStats.query() / client.$procs.send.mutate({ args }). Sequential tx: client.$transaction([{ model, op, args }, ...]).

TanStack Query hooks (RPC APIs only; React 18+/Vue 3+/Svelte 5.25+)

Install @zenstackhq/tanstack-query and the matching @tanstack/*-query. Provide settings near the root (React shown; Vue uses provideQuerySettingsContext, Svelte setQuerySettingsContext):

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QuerySettingsProvider } from '@zenstackhq/tanstack-query/react';

<QueryClientProvider client={new QueryClient()}>
    <QuerySettingsProvider value={{ endpoint: '/api/model' }}>
        {children}
    </QuerySettingsProvider>
</QueryClientProvider>;

Hooks mirror the ORM client via useClientQueries(schema):

import { useClientQueries } from '@zenstackhq/tanstack-query/react';

const client = useClientQueries(schema);
const { data } = client.user.useFindMany({ include: { posts: true } });
const create = client.post.useCreate();
create.mutate({ data: { title: 'New post' } });
  • Auto invalidation: mutations invalidate affected queries automatically (respects nested reads/writes and cascades). Opt out per mutation with { invalidateQueries: false }.
  • Optimistic updates: client.post.useCreate({ optimisticUpdate: true }); customize with optimisticUpdateProvider.
  • Also: useInfiniteFindMany, $procs.<name>.useQuery()/useMutation(), $transaction.useSequential(), and DbNull/JsonNull/AnyNull exports for JSON nulls.

A community Pinia Colada integration exists as zenstack-pinia-colada (Vue 3).

OpenAPI spec

Both handlers can emit an OpenAPI spec via generateSpec() (RPC v3.6.0+, REST v3.5.0+):

import type { OpenApiSpecOptions } from '@zenstackhq/server/api';

app.get('/api/openapi.json', async (_req, res) => {
    const spec = await apiHandler.generateSpec({
        title: 'My Blog API',
        version: '2.0.0',
        respectAccessPolicies: true, // emit 403 responses for policy-protected models
    });
    res.json(spec);
});

Options: title (default 'ZenStack Generated API'), version (default '1.0.0'), description, summary, respectAccessPolicies (default false). queryOptions.slicing/omit on the handler also shape what appears in the spec.

Reference docs

Full ZenStack documentation for this topic is bundled under references/:

What ships with it: 20 files

1000 B alongside SKILL.md

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.