diff --git a/docs/reference/plugins/openapi.md b/docs/reference/plugins/openapi.md
index 86bbfbf8..8a2f59ec 100644
--- a/docs/reference/plugins/openapi.md
+++ b/docs/reference/plugins/openapi.md
@@ -1,6 +1,6 @@
 ---
 description: Plugin for generating OpenAPI 3.0 specifications
-sidebar_position: 4
+sidebar_position: 5
 ---
 
 # @zenstackhq/openapi
diff --git a/docs/reference/plugins/prisma.md b/docs/reference/plugins/prisma.md
new file mode 100644
index 00000000..36b8ba9d
--- /dev/null
+++ b/docs/reference/plugins/prisma.md
@@ -0,0 +1,25 @@
+---
+description: Built-in plugin for generating Prisma schema
+sidebar_position: 1
+---
+
+# @core/prisma
+
+The `@core/prisma` plugin generates a Prisma schema from ZModel. This plugin is always automatically included when `zenstack generate` is run. You only need to add it to your ZModel if you want to customize its options.
+
+## Options
+
+| Name           | Type    | Description                                       | Required | Default                |
+| -------------- | ------- | ------------------------------------------------- | -------- | ---------------------- |
+| output         | String  | Output file path                                  | No       | ./prisma/schema.prisma |
+| generateClient | Boolean | Whether to run `prisma generate` after generation | No       | true                   |
+
+## Example
+
+```prisma title='/schema.zmodel'
+plugin prisma {
+    provider = '@core/prisma'
+    output = './src/prisma/schema.prisma'
+    generateClient = false
+}
+```
diff --git a/docs/reference/plugins/react.md b/docs/reference/plugins/react.md
index ab0db72c..6ac27a83 100644
--- a/docs/reference/plugins/react.md
+++ b/docs/reference/plugins/react.md
@@ -1,6 +1,6 @@
 ---
 description: Plugin for generating React data query hooks
-sidebar_position: 1
+sidebar_position: 3
 ---
 
 # @zenstackhq/react
diff --git a/docs/reference/plugins/trpc.md b/docs/reference/plugins/trpc.md
index 2516fecb..bdb900ac 100644
--- a/docs/reference/plugins/trpc.md
+++ b/docs/reference/plugins/trpc.md
@@ -1,6 +1,6 @@
 ---
 description: Plugin for generating tRPC data query routes
-sidebar_position: 2
+sidebar_position: 4
 ---
 
 # @zenstackhq/trpc
diff --git a/docs/reference/plugins/zod.md b/docs/reference/plugins/zod.md
new file mode 100644
index 00000000..4435c961
--- /dev/null
+++ b/docs/reference/plugins/zod.md
@@ -0,0 +1,89 @@
+---
+description: Built-in plugin for generating Zod schemas for validating CRUD input arguments
+sidebar_position: 2
+---
+
+# @core/zod
+
+The `@core/zod` plugin generates [Zod](https://github.com/colinhacks/zod) schemas for input arguments of Prisma CRUD operations.
+
+The hooks syntactically mirror the APIs of a standard Prisma client, including the function names and shapes of parameters (hooks directly use types generated by Prisma). At runtime, the hooks use [SWR](https://swr.vercel.app/) to query and mutate so that you can enjoy its features like caching, automatic revalidation, etc.
+
+## Options
+
+| Name   | Type   | Description      | Required |
+| ------ | ------ | ---------------- | -------- |
+| output | String | Output directory | Yes      |
+
+## Example
+
+Here's an example with a blogging app:
+
+```prisma title='/schema.zmodel'
+plugin reactHooks {
+  provider = '@zenstackhq/react'
+  output = "./src/lib/hooks"
+}
+
+model User {
+  id            String    @id @default(cuid())
+  email         String
+  posts         Post[]
+
+  // everyone can signup, and user profile is also publicly readable
+  @@allow('create,read', true)
+}
+
+model Post {
+  id        String @id @default(cuid())
+  title     String
+  published Boolean @default(false)
+  author    User @relation(fields: [authorId], references: [id])
+  authorId  String
+
+  // author has full access
+  @@allow('all', auth() == author)
+
+  // logged-in users can view published posts
+  @@allow('read', auth() != null && published)
+}
+```
+
+```tsx title='/src/components/posts.tsx'
+import type { Post } from '@prisma/client';
+import { usePost } from '../lib/hooks';
+
+// post list component
+const Posts = ({ userId }: { userId: string }) => {
+    // Post crud hooks
+    const { findMany, create } = usePost();
+
+    // list all posts that're visible to the current user, together with their authors
+    const { data: posts } = findMany({
+        include: { author: true },
+        orderBy: { createdAt: 'desc' },
+    });
+
+    async function onCreatePost() {
+        await create({
+            data: {
+                title: 'My awesome post',
+                authorId: userId,
+            },
+        });
+    }
+
+    return (
+        <>
+            <button onClick={onCreatePost}>Create</button>
+            <ul>
+                {posts?.map((post) => (
+                    <li key={post.id}>
+                        {post.title} by {post.author.email}
+                    </li>
+                ))}
+            </ul>
+        </>
+    );
+};
+```
diff --git a/docs/reference/server-adapters/_api-request.md b/docs/reference/server-adapters/_api-request.md
new file mode 100644
index 00000000..096d35a5
--- /dev/null
+++ b/docs/reference/server-adapters/_api-request.md
@@ -0,0 +1,47 @@
+You can use any HTTP client to consume the APIs. Here's some samples using [node-fetch](https://github.com/node-fetch/node-fetch):
+
+```ts
+const base = 'http://localhost:8080/api/openapi';
+
+// create a user with two posts
+const r = await fetch(`${base}/user/create`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+        include: { posts: true },
+        data: {
+            email: 'user1@abc.com',
+            posts: {
+                create: [{ title: 'Post 1' }, { title: 'Post 2' }],
+            },
+        },
+    }),
+});
+
+console.log(await r.json());
+```
+
+Output:
+
+```json
+{
+    "id": "clf7y7k840000vhs3iej7skve",
+    "email": "user1@abc.com",
+    "posts": [
+        {
+            "id": "clf7y7k840001vhs377njs07l",
+            "createdAt": "2023-03-14T07:45:04.036Z",
+            "updatedAt": "2023-03-14T07:45:04.036Z",
+            "title": "Post 1",
+            "authorId": "clf7y7k840000vhs3iej7skve"
+        },
+        {
+            "id": "clf7y7k840002vhs3w11iitmi",
+            "createdAt": "2023-03-14T07:45:04.036Z",
+            "updatedAt": "2023-03-14T07:45:04.036Z",
+            "title": "Post 2",
+            "authorId": "clf7y7k840000vhs3iej7skve"
+        }
+    ]
+}
+```
diff --git a/docs/reference/server-adapters/_common-options.md b/docs/reference/server-adapters/_common-options.md
new file mode 100644
index 00000000..2b5cbb38
--- /dev/null
+++ b/docs/reference/server-adapters/_common-options.md
@@ -0,0 +1,9 @@
+-   getPrisma: `(request: FastifyRequest, reply: FastifyReply) => unknown | Promise<unknown>`
+
+    A callback for getting a PrismaClient instance for talking to the database. Usually you'll use an enhanced instance created with ZenStack's [`withPresets`](/docs/reference/runtime-api#withpresets) or [`withPolicy`](/docs/reference/runtime-api#withpolicy) APIs to ensure access policies are enforced.
+
+-   zodSchemas: `ModelZodSchema | boolean | undefined`
+
+    Provides the Zod schemas used for validating CRUD request input. The Zod schemas can be generated with the `@core/zod` plugin. Pass `true` for this option to load the schemas from the default location. If you configured `@core/zod` plugin to output to a custom location, you can load the schemas explicitly and pass the loaded module to this option.
+
+    Not passing this option or passing in `undefined` disables input validation.
diff --git a/docs/reference/server-adapters/express.mdx b/docs/reference/server-adapters/express.mdx
index 14e21a83..833b02a7 100644
--- a/docs/reference/server-adapters/express.mdx
+++ b/docs/reference/server-adapters/express.mdx
@@ -3,6 +3,47 @@ description: Adapter for integrating with Express.js
 sidebar_position: 3
 ---
 
-# 🚧 Express.js
+import ErrorHandling from './_error-handling.md';
+import ApiRequest from './_api-request.md';
+import CommonOptions from './_common-options.md';
 
-Coming soon.
+# Express.js
+
+The `@zenstackhq/server/express` module provides a quick way to install OpenAPI endpoints onto a [Express.js](https://expressjs.com/) project for exposing database CRUD. Combined with ZenStack's power of enhancing Prisma with access policies, it's surprisingly simple to achieve a secure data backend without manually coding it.
+
+### Mounting the APIs
+
+You can integrate ZenStack into your project with the `ZenStackFastifyPlugin` [fastify plugin](https://www.fastify.io/docs/latest/Reference/Plugins/):
+
+```ts
+import { PrismaClient } from '@prisma/client';
+import { withPresets } from '@zenstackhq/runtime';
+import { ZenStackMiddleware } from '@zenstackhq/server/express';
+import express from 'express';
+
+const prisma = new PrismaClient();
+const app = express();
+
+app.use(express.json());
+
+app.use(
+    '/api/openapi',
+    ZenStackMiddleware({
+        // getSessionUser extracts the current session user from the request, its
+        // implementation depends on your auth solution
+        getPrisma: (request) => withPresets(prisma, { user: getSessionUser(request) }),
+    })
+);
+```
+
+The Express.js adapter takes the following options to initialize:
+
+<CommonOptions />
+
+### Using the APIs
+
+The APIs mounted by `ZenStackFastifyPlugin` conforms to the OpenAPI V3 spec, and provides a path for each model and database operation combination, as listed [here](/docs/reference/plugins/openapi#generated-api-paths).
+
+<ApiRequest />
+
+<ErrorHandling />
diff --git a/docs/reference/server-adapters/fastify.mdx b/docs/reference/server-adapters/fastify.mdx
index 11015b58..053fa320 100644
--- a/docs/reference/server-adapters/fastify.mdx
+++ b/docs/reference/server-adapters/fastify.mdx
@@ -4,6 +4,8 @@ sidebar_position: 2
 ---
 
 import ErrorHandling from './_error-handling.md';
+import ApiRequest from './_api-request.md';
+import CommonOptions from './_common-options.md';
 
 # Fastify
 
@@ -14,7 +16,7 @@ The `@zenstackhq/server/fastify` module provides a quick way to install OpenAPI
 You can integrate ZenStack into your project with the `ZenStackFastifyPlugin` [fastify plugin](https://www.fastify.io/docs/latest/Reference/Plugins/):
 
 ```ts
-import { withPreset } from '@zenstackhq/runtime';
+import { withPresets } from '@zenstackhq/runtime';
 import { ZenStackFastifyPlugin } from '@zenstackhq/server/fastify';
 import { prisma } from './db.ts';
 import { getSessionUser } from './auth.ts';
@@ -30,72 +32,18 @@ server.register(ZenStackFastifyPlugin, {
 });
 ```
 
-The fastify plugin takes two options to initialize:
+The Fastify adapter takes the following options to initialize:
 
--   prefix
+-   prefix: `string`
 
     The API path to install the OpenAPI CRUD endpoints.
 
--   getPrisma
-
-    ```ts
-    (request: FastifyRequest, reply: FastifyReply) => unknown | Promise<unknown>;
-    ```
-
-    A callback for getting a PrismaClient instance for talking to the database. Usually you'll use an enhanced instance created with ZenStack's [`withPresets`](/docs/reference/runtime-api#withpresets) or [`withPolicy`](/docs/reference/runtime-api#withpolicy) APIs to ensure access policies are enforced.
+<CommonOptions />
 
 ### Using the APIs
 
 The APIs mounted by `ZenStackFastifyPlugin` conforms to the OpenAPI V3 spec, and provides a path for each model and database operation combination, as listed [here](/docs/reference/plugins/openapi#generated-api-paths).
 
-You can use any HTTP client to consume the APIs. Here's some samples using [node-fetch](https://github.com/node-fetch/node-fetch):
-
-```ts
-const base = 'http://localhost:8080/api/openapi';
-
-// create a user with two posts
-const r = await fetch(`${base}/user/create`, {
-    method: 'POST',
-    headers: { 'Content-Type': 'application/json' },
-    body: JSON.stringify({
-        include: { posts: true },
-        data: {
-            email: 'user1@abc.com',
-            posts: {
-                create: [{ title: 'Post 1' }, { title: 'Post 2' }],
-            },
-        },
-    }),
-});
-
-console.log(await r.json());
-```
-
-Output:
-
-```json
-{
-    "id": "clf7y7k840000vhs3iej7skve",
-    "email": "user1@abc.com",
-    "zenstack_guard": true,
-    "zenstack_transaction": null,
-    "posts": [
-        {
-            "id": "clf7y7k840001vhs377njs07l",
-            "createdAt": "2023-03-14T07:45:04.036Z",
-            "updatedAt": "2023-03-14T07:45:04.036Z",
-            "title": "Post 1",
-            "authorId": "clf7y7k840000vhs3iej7skve"
-        },
-        {
-            "id": "clf7y7k840002vhs3w11iitmi",
-            "createdAt": "2023-03-14T07:45:04.036Z",
-            "updatedAt": "2023-03-14T07:45:04.036Z",
-            "title": "Post 2",
-            "authorId": "clf7y7k840000vhs3iej7skve"
-        }
-    ]
-}
-```
+<ApiRequest />
 
 <ErrorHandling />
