zenstack-crud-server

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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

ZenStack V3 — 自动CRUD服务(查询即服务)

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
.
ZenStack 可将你的数据模型转换为安全的CRUD Web API,无需编写控制器代码。它由两部分组成:
  • API处理器 — 与框架无关;定义API风格(RPC或RESTful)并将HTTP请求转换为ORM查询。来自
    @zenstackhq/server/api
  • 服务器适配器 — 框架专属的适配层,将处理器安装到Express/Next.js等框架中,并提供每个请求的、经过策略校验的ORM客户端。来自
    @zenstackhq/server/<framework>
HTTP请求 → 服务器适配器 → API处理器(RPC | REST) → ZenStack ORM(应用策略) → 数据库
此功能基于其他技能构建:使用
zenstack-schema-modeling
定义架构、使用
zenstack-access-control
保障安全、使用
zenstack-querying
创建基础客户端。安装服务器包:
npm install @zenstackhq/server

API handlers

API处理器

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: ... }
.
ts
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.
ts
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:
ts
new RPCApiHandler({
    schema,
    queryOptions: {
        slicing: {
            includedModels: ['User', 'Post'],
            models: { post: { excludedOperations: ['delete'] } },
        },
        omit: { user: { password: true } },
    },
});
RPC — 与ORM API 1:1镜像。路由格式如
GET /post/findMany?q=<urlencoded-json>
POST /post/create
(请求体
{ data: ... }
);响应包裹在
{ data: ... }
中。
ts
import { RPCApiHandler } from '@zenstackhq/server/api';
import { schema } from '~/zenstack/schema';

const apiHandler = new RPCApiHandler({ schema });
RPC端点:支持所有ORM操作(
findMany
,
findUnique
,
findFirst
,
count
,
aggregate
,
groupBy
,
create
,
createMany
,
createManyAndReturn
,
upsert
,
update
,
updateMany
,
updateManyAndReturn
,
delete
,
deleteMany
),此外还有自定义过程的
$procs/<name>
和事务接口
$transaction/sequential
。状态码:创建成功返回
201
,其他成功返回
200
,请求格式错误返回
400
,策略校验失败返回
403
,资源不存在返回
404
,验证错误返回
422
,意外错误返回
500
RESTful — 符合JSON:API v1.1规范。路由格式如
GET /post
,
GET /post/:id
,
POST /post
,
PUT|PATCH /post/:id
,
DELETE /post/:id
,以及关联关系路由。
ts
import { RestApiHandler } from '@zenstackhq/server/api'; // 注意:是RestApiHandler,不是RESTful

const apiHandler = new RestApiHandler({
    schema,
    endpoint: 'http://localhost:3000/api', // 必填 — 用于构建资源链接
});
RestApiHandler
配置项:
endpoint
(必填)、
pageSize
(默认100;设为
Infinity
禁用分页)、
modelNameMapping
(如
{ User: 'users' }
)、
externalIdMapping
(如
{ Tag: 'name' }
)、
nestedRoutes
(布尔值)。REST查询参数:
filter[field]=
,
filter[field$op]=
(如
$lt
,
$gt
,
$contains
,
$startsWith
等)、
sort=field,-other
,
page[offset]=
/
page[limit]=
,
include=rel,rel.nested
,
fields[type]=a,b
两种处理器都支持
queryOptions
配置,用于筛选/隐藏API暴露的内容:
ts
new RPCApiHandler({
    schema,
    queryOptions: {
        slicing: {
            includedModels: ['User', 'Post'],
            models: { post: { excludedOperations: ['delete'] } },
        },
        omit: { user: { password: true } },
    },
});

Server adapters —
getClient
is where access control lives

服务器适配器 —
getClient
是访问控制的核心

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:
ts
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).
每个适配器都接收
apiHandler
getClient(request) => ClientContract
回调函数。需返回一个通过
$setAuth
绑定当前用户的策略校验客户端(详见
zenstack-access-control
),确保每个请求相互隔离:
ts
getClient: (req) => authDb.$setAuth(getSessionUser(req)),
authDb
db.$use(new PolicyPlugin())
创建的实例。匿名请求返回
authDb.$setAuth(undefined)
,如需绕过策略校验可直接返回原始
db
(公开API中极少使用)。

Express

Express

ts
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.)
ts
import { ZenStackMiddleware } from '@zenstackhq/server/express';

app.use(express.json());
app.use(
    '/api/model',
    ZenStackMiddleware({
        apiHandler,
        getClient: (req) => authDb.$setAuth(getSessionUser(req)),
    }),
);
(可选配置
sendResponse: false
,会将结果写入
res.locals
并调用
next()
,而非直接返回响应。)

Fastify

Fastify

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

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

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

Next.js — App Router

Next.js — App Router

ts
// 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,
};
ts
// 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

Next.js — Pages Router

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

export default NextRequestHandler({
    apiHandler,
    getClient: (req, res) => authDb.$setAuth(getSessionUser(req, res)),
});
ts
// 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

Nuxt

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

export default createEventHandler({
    apiHandler,
    getClient: (event) => authDb.$setAuth(getSessionUser(event)),
});
ts
// 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
)

SvelteKit(API路由 — 推荐;通配符参数必须命名为
path

ts
// 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.)
ts
// 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;
hooks.server.ts
中的旧版
SvelteKitHandler
prefix
配置已被废弃。)

Hono

Hono

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

app.use(
    '/api/model/*',
    createHonoHandler({
        apiHandler,
        getClient: (ctx) => authDb.$setAuth(getSessionUser(ctx)),
    }),
);
ts
import { createHonoHandler } from '@zenstackhq/server/hono';

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

Elysia

Elysia

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

app.group('/crud', (app) =>
    app.use(
        createElysiaHandler({
            apiHandler,
            basePath: '/api/model',
            getClient: (ctx) => authDb.$setAuth(getSessionUser(ctx)),
        }),
    ),
);
ts
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

TanStack Start

ts
// 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,
        },
    },
});
ts
// 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.
处理器使用superjson实现非JSON类型的传输:
DateTime
转换为ISO字符串,
Bytes
转换为base64,
BigInt
/
Decimal
转换为字符串。生成的客户端SDK会自动处理该逻辑。如果手动构造请求,需在
serialization
键下包含superjson的
meta
信息(格式为
{ "data": ..., "meta": { "serialization": <meta> } }
);响应也会携带相同的信息。

Consuming the API

API调用

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

Fetch客户端(v3.7.0+,仅支持RPC API)

ts
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
:
ts
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 }, ...])
.
ts
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' } });
通过自定义
fetch
添加认证:
ts
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 });
自定义过程调用:
client.$procs.getStats.query()
/
client.$procs.send.mutate({ args })
。 顺序事务:
client.$transaction([{ model, op, args }, ...])

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

TanStack Query钩子(仅支持RPC API;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
):
tsx
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)
:
ts
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).
安装
@zenstackhq/tanstack-query
及对应的
@tanstack/*-query
包。在根组件附近提供配置(以下为React示例;Vue使用
provideQuerySettingsContext
,Svelte使用
setQuerySettingsContext
):
tsx
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>;
通过
useClientQueries(schema)
获取与ORM客户端镜像的钩子:
ts
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' } });
  • 自动失效:变更操作会自动使受影响的查询失效(支持嵌套读写和级联操作)。可通过
    { invalidateQueries: false }
    在单个变更中关闭此功能。
  • 乐观更新:配置
    client.post.useCreate({ optimisticUpdate: true })
    启用;可通过
    optimisticUpdateProvider
    自定义逻辑。
  • 其他功能:
    useInfiniteFindMany
    $procs.<name>.useQuery()/useMutation()
    $transaction.useSequential()
    ,以及用于JSON空值的
    DbNull
    /
    JsonNull
    /
    AnyNull
    导出。
社区提供了Pinia Colada集成包
zenstack-pinia-colada
(适用于Vue 3)。

OpenAPI spec

OpenAPI规范

Both handlers can emit an OpenAPI spec via
generateSpec()
(RPC v3.6.0+, REST v3.5.0+):
ts
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.
两种处理器都可通过
generateSpec()
生成OpenAPI规范(RPC从v3.6.0+支持,REST从v3.5.0+支持):
ts
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, // 为受策略保护的模型生成403响应定义
    });
    res.json(spec);
});
配置项:
title
(默认
'ZenStack Generated API'
)、
version
(默认
'1.0.0'
)、
description
summary
respectAccessPolicies
(默认false)。处理器的
queryOptions.slicing
/
omit
配置也会影响规范内容。

Reference docs

参考文档

Full ZenStack documentation for this topic is bundled under
references/
:
  • service-overview.md — automatic CRUD service overview
  • server-adapter.md — server adapter concepts
  • api-handler-overview.md — API handlers overview
  • api-handler-rpc.md — RPC API handler
  • api-handler-rest.md — RESTful API handler
  • client-sdk-overview.md — client SDK overview
  • fetch-client.md — fetch client
  • tanstack-query.md — TanStack Query hooks
  • openapi-overview.md — OpenAPI overview
  • openapi-rpc.md — OpenAPI for RPC
  • openapi-restful.md — OpenAPI for REST
  • api-reference.md — server API reference
  • Server adapters: express, fastify, next, nuxt, sveltekit, hono, elysia, tanstack-start
关于此主题的完整ZenStack文档打包在
references/
目录下:
  • service-overview.md — 自动CRUD服务概述
  • server-adapter.md — 服务器适配器概念
  • api-handler-overview.md — API处理器概述
  • api-handler-rpc.md — RPC API处理器
  • api-handler-rest.md — RESTful API处理器
  • client-sdk-overview.md — 客户端SDK概述
  • fetch-client.md — Fetch客户端
  • tanstack-query.md — TanStack Query钩子
  • openapi-overview.md — OpenAPI概述
  • openapi-rpc.md — RPC的OpenAPI规范
  • openapi-restful.md — REST的OpenAPI规范
  • api-reference.md — 服务器API参考
  • 服务器适配器:expressfastifynextnuxtsveltekithonoelysiatanstack-start