Next.js Server Adapter
The @zenstackhq/server/next
module provides a quick way to install API endpoints onto a Next.js project for database CRUD operations. 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.
The server adapter supports both the "pages" routes for older versions of Next.js and the new "app" routes for Next.js 13.
Installation
npm install @zenstackhq/server
Mounting the API
You can use it to create a request handler in an API endpoint like:
- App Router
- Pages Router
import { NextRequestHandler } from '@zenstackhq/server/next';
import type { NextRequest } from "next/server";
import { enhance } from '@zenstackhq/runtime';
import { prisma } from '~/lib/db.ts';
import { getSessionUser } from '~/lib/auth.ts';
// create an enhanced Prisma client with user context
function getPrisma(req: NextRequest) {
// getSessionUser extracts the current session user from the request, its
// implementation depends on your auth solution
return enhance(prisma, { user: getSessionUser(req) });
}
const handler = NextRequestHandler({ getPrisma, useAppDir: true });
export {
handler as GET,
handler as POST,
handler as PUT,
handler as PATCH,
handler as DELETE,
};
Next.js 13 has issues handling @zenstackhq/runtime
and @zenstackhq/server
packages when compiling server components. Please add the following config section to your next.config.mjs
to exclude them:
const config = {
experimental: {
serverComponentsExternalPackages: [
"@zenstackhq/runtime",
"@zenstackhq/server",
],
},
}
The Next.js API route handler takes the following options to initialize:
-
getPrisma (required)
(req: NextRequest) => 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
enhance
API to ensure access policies are enforced. -
logger (optional)
LoggerConfig
Configuration for customizing logging behavior.
-
modelMeta (optional)
ModelMeta
Model metadata. By default loaded from the
node_module/.zenstack/model-meta
module. You can pass it in explicitly if you configured ZenStack to output to a different location. E.g.:require('output/model-meta').default
. -
zodSchemas (optional)
ModelZodSchema | boolean | undefined
Provides the Zod schemas used for validating CRUD request input. The Zod schemas can be generated with the
@core/zod
plugin. Passtrue
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. E.g.:factory({
...
zodSchemas: require('./zod'),
});Not passing this option or passing in
undefined
disables input validation. -
useSuperJson (optional)
boolean
Whether to use superjson for data serialization. Defaults to
false
.NOTE: This options is decprecated. The server adapters now always use superjson for data serialization.
-
handler (optional)
(req: RequestContext) => Promise<Response>
The request handler function. This option determines the API endpoints and its input and output formats. Currently ZenStack supports two styles of APIs: RPC (the default) and RESTful.
-
RPC
The goal of the RPC-style API handler is to fully mirror PrismaClient's API across the network, so that developers can continue enjoying the convenience and flexibility of Prisma's query syntax. This is the default choice for the
handler
option.The RPC-style handler can be created like:
import RPCApiHandler from '@zenstackhq/server/api/rpc';
const handler = RPCApiHandler();For more details, please check out RPC API Handler.
-
RESTful
The goal of RESTful-style API handler is to provide a resource-centric RESTful API using JSON:API as transportation format.
The RESTful-style handler can be created like:
import RestApiHandler from '@zenstackhq/server/api/rest';
const handler = RestApiHandler({ endpoint: 'http://myhost/api' });For more details, please check out RESTful API Handler.
-
import { NextRequestHandler } from '@zenstackhq/server/next';
import type { NextApiRequest, NextApiResponse } from 'next';
import { enhance } from '@zenstackhq/runtime';
import { prisma } from '~/lib/db.ts';
import { getSessionUser } from '~/lib/auth.ts';
// create an enhanced Prisma client with user context
function getPrisma(req: NextApiRequest, res: NextApiResponse) {
// getSessionUser extracts the current session user from the request, its
// implementation depends on your auth solution
return enhance(prisma, { user: getSessionUser(req, res) });
}
// create the request handler with the `getPrisma` hook
export default NextRequestHandler({ getPrisma });
The Next.js API route handler takes the following options to initialize:
-
getPrisma (required)
(req: NextApiRequest, res: NextApiResponse) => 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
enhance
API to ensure access policies are enforced. -
logger (optional)
LoggerConfig
Configuration for customizing logging behavior.
-
modelMeta (optional)
ModelMeta
Model metadata. By default loaded from the
node_module/.zenstack/model-meta
module. You can pass it in explicitly if you configured ZenStack to output to a different location. E.g.:require('output/model-meta').default
. -
zodSchemas (optional)
ModelZodSchema | boolean | undefined
Provides the Zod schemas used for validating CRUD request input. The Zod schemas can be generated with the
@core/zod
plugin. Passtrue
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. E.g.:factory({
...
zodSchemas: require('./zod'),
});Not passing this option or passing in
undefined
disables input validation. -
useSuperJson (optional)
boolean
Whether to use superjson for data serialization. Defaults to
false
.NOTE: This options is decprecated. The server adapters now always use superjson for data serialization.
-
handler (optional)
(req: RequestContext) => Promise<Response>
The request handler function. This option determines the API endpoints and its input and output formats. Currently ZenStack supports two styles of APIs: RPC (the default) and RESTful.
-
RPC
The goal of the RPC-style API handler is to fully mirror PrismaClient's API across the network, so that developers can continue enjoying the convenience and flexibility of Prisma's query syntax. This is the default choice for the
handler
option.The RPC-style handler can be created like:
import RPCApiHandler from '@zenstackhq/server/api/rpc';
const handler = RPCApiHandler();For more details, please check out RPC API Handler.
-
RESTful
The goal of RESTful-style API handler is to provide a resource-centric RESTful API using JSON:API as transportation format.
The RESTful-style handler can be created like:
import RestApiHandler from '@zenstackhq/server/api/rest';
const handler = RestApiHandler({ endpoint: 'http://myhost/api' });For more details, please check out RESTful API Handler.
-
Controlling what endpoints to expose
You can use a Next.js middleware to further control what endpoints to expose. For example, if you're using a RESTful API handler installed at "/api/model", you can disallow listing all User
entities by adding a middleware like:
import { type NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const url = new URL(request.url);
if (
request.method === 'GET' &&
url.pathname.match(/^\/api\/model\/user\/?$/)
) {
return NextResponse.json({ error: 'Not allowed' }, { status: 405 });
}
}
export const config = {
matcher: '/api/model/:path*',
};
Using the API
The APIs can be used in the following three ways:
-
With generated client hooks
ZenStack provides plugins to generate client hooks from the ZModel targeting the most popular frontend data fetching libraries: TanStack Query and SWR. The generated hooks can be used to make API calls to the server adapters. Refer to the follow docs for detailed usage:
infoThe generated client hooks assumes the server adapter uses RPC-style API handler (which is the default setting).
-
With direct HTTP calls
You can make direct HTTP calls to the server adapter using your favorite client libraries like
fetch
oraxios
. Refer to the documentation of the API Handlers for the API endpoints and data formats.Here's an example using
fetch
:- RPC Handler
- RESTful Handler
// create a user with two posts
const r = await fetch(`/api/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:
{
"id": 1,
"email": "user1@abc.com",
"posts": [
{
"id": 1,
"createdAt": "2023-03-14T07:45:04.036Z",
"updatedAt": "2023-03-14T07:45:04.036Z",
"title": "Post 1",
"authorId": 1
},
{
"id": 2,
"createdAt": "2023-03-14T07:45:04.036Z",
"updatedAt": "2023-03-14T07:45:04.036Z",
"title": "Post 2",
"authorId": 1
}
]
}// create a user and attach two posts
const r = await fetch(`/api/user`, {
method: 'POST',
headers: { 'Content-Type': 'application/vnd.api+json' },
body: JSON.stringify({
data: {
type: 'user',
attributes: {
email: 'user1@abc.com'
},
relationships: {
posts: {
data: [
{ type: 'post', id: 1 },
{ type: 'post', id: 2 }
]
}
}
}
})
});
console.log(await r.json());Output:
{
"jsonapi": { "version": "1.1" },
"data": {
"type": "user",
"id": 1,
"attributes": {
"email": "user1@abc.com",
},
"links": {
"self": "http://localhost/api/user/1",
},
"relationships": {
"posts": {
"links": {
"self": "http://localhost/api/user/1/relationships/posts",
"related": "http://localhost/api/user/1/posts",
},
"data": [
{ "type": "post", "id": 1 },
{ "type": "post", "id": 2 },
],
},
},
},
} -
With third-party client generators
ZenStack provides an OpenAPI plugin for generating Open API 3.x specification from the ZModel. The generated OpenAPI spec can be used to generate client libraries for various languages and frameworks. For example, you can use openapi-typescript to generate a typescript client.
Error Handling
Refer to the specific sections for RPC Handler and RESTful Handler.
Fully working example
You can find the fully working examples below: