zenstack-crud-server について
zenstack-crud-serverは、データモデルからCRUDサービスを構築するZenStack V3のスキルです。RPC・RESTハンドラー、ポリシー適用クライアント、複数のWebフレームワーク向けアダプターを提供します。
主な機能
- 01RPC・REST APIの生成
- 02リクエストごとのポリシー適用ORMクライアント
- 03サーバーフレームワークへの統合
- 04公開するモデルや操作の制限
- 05OpenAPI仕様の生成
- 06非JSONデータ型のシリアライズ
活用シーン
- 01データモデルにCRUDエンドポイントを追加
- 02リクエストごとにユーザーポリシーを適用
- 03ExpressやNext.jsにAPIを統合
- 04OpenAPI仕様を公開
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zenstackhq/ skills --skill zenstack-crud-serverインストール後、Claude Code で/zenstack-crud-serverを入力して読み込まれたか確認します。
詳細
ZenStack V3 — 自動 CRUD サービス(サービスとしてのクエリ)
ZenStack は、手書きのコントローラーを作成せずに、データモデルをセキュアな CRUD Web API に変換します。次の 2 つの要素を組み合わせます。
- API ハンドラー — フレームワークに依存せず、API のスタイル(RPC または RESTful)を定義し、HTTP リクエストを ORM クエリに変換します。
@zenstackhq/server/apiから利用できます。 - サーバーアダプター — フレームワーク固有の連携機能です。ハンドラーを Express/Next.js などに登録し、リクエストごとのポリシー適用済み ORM クライアントを提供します。
@zenstackhq/server/<framework>から利用できます。
HTTP request → server adapter → API handler (RPC | REST) → ZenStack ORM (policies applied) → DB
これは他のスキルを基盤としています。zenstack-schema-modeling でスキーマを定義し、zenstack-access-control で保護し、zenstack-querying でベースクライアントを作成します。サーバーパッケージをインストールします:npm install @zenstackhq/server。
API ハンドラー
RPC — ORM API を 1:1 で反映します。ルートは GET /post/findMany?q=<urlencoded-json> や POST /post/create(本文 { data: ... })のようになり、レスポンスは { data: ... } でラップされます。
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 と、リレーションシップのルートなどです。
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 のオプション: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。
どちらのハンドラーも、API が公開する内容を絞り込み/除外するための queryOptions を受け取ります。
new RPCApiHandler({
schema,
queryOptions: {
slicing: {
includedModels: ['User', 'Post'],
models: { post: { excludedOperations: ['delete'] } },
},
omit: { user: { password: true } },
},
});
サーバーアダプター — アクセス制御は getClient で設定
すべてのアダプターは apiHandler と getClient(request) => ClientContract コールバックを受け取ります。リクエストごとに分離されるよう、$setAuth を介して現在のユーザーに紐づいたポリシー適用済みクライアントを返します(zenstack-access-control を参照)。
getClient: (req) => authDb.$setAuth(getSessionUser(req)),
authDb は db.$use(new PolicyPlugin()) です。匿名ユーザーには authDb.$setAuth(undefined) を返し、ポリシーを回避する場合(公開 API では通常必要ありません)は生の db を返します。
Express
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
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 ルート — 推奨。ワイルドカードパラメーター名は 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;
(prefix オプションを使う hooks.server.ts のレガシーな SvelteKitHandler は非推奨です。)
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,
},
},
});
データ型のシリアル化
ハンドラーは superjson を使用するため、JSON では扱えない型も通信時に保持されます:DateTime → ISO 文字列、Bytes → base64、BigInt/Decimal → 文字列。生成されたクライアント SDK はこれを自動的に処理します。リクエストを手動で作成する場合は、superjson の meta を serialization キーの下に含めます
({ "data": ..., "meta": { "serialization": <meta> } })。レスポンスにも同じ情報が含まれます。
API の利用
Fetch クライアント(v3.7.0 以降、RPC API のみ)
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 を使って認証を追加します。
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 })。
シーケンシャル tx:client.$transaction([{ model, op, args }, ...])。
TanStack Query フック(RPC API のみ。React 18+/Vue 3+/Svelte 5.25+)
@zenstackhq/tanstack-query と、対応する @tanstack/*-query をインストールします。ルート付近で設定を指定します(React の例を示します。Vue では 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>;
useClientQueries(schema) を使って、ORM クライアントに対応するフックを利用できます。
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()、および JSON の null 値に使用するDbNull/JsonNull/AnyNullエクスポート。
コミュニティによる Pinia Colada 統合として、zenstack-pinia-colada(Vue 3)が用意されています。
OpenAPI 仕様
どちらのハンドラーでも、generateSpec() を使って OpenAPI 仕様を生成できます(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);
});
オプション:title(デフォルトは 'ZenStack Generated API')、version(デフォルトは '1.0.0')、description、summary、respectAccessPolicies(デフォルトは false)。ハンドラーの queryOptions.slicing/omit も、仕様に含まれる内容に影響します。
リファレンスドキュメント
このトピックに関する 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 リファレンス
- サーバーアダプター:express、fastify、next、nuxt、sveltekit、hono、elysia、tanstack-start