agentsclimarketplace

Zenstack crud server

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

Agent skills for ZenStack, published to skills.sh — ZModel schema modeling, access control, querying, automatic CRUD service, migrations, and plugin development.

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.

One thing 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.

What its author says it does

Copied from the file, not written here

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).

SKILL.md

11.5 KB, 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/:

Keep looking

Skills are one crate of 328,083. 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.