RPC API Handler
Introduction
The RPC-style API handler exposes CRUD endpoints that fully mirror PrismaClient's query API. Consuming the APIs feels like making RPC calls to a PrismaClient then. The API handler is not meant to be used directly; instead, you should use it together with a server adapter which handles the request and response API for a specific framework.
It can be created and used as the following:
- Next.js
- SvelteKit
- Nuxt
import { NextRequestHandler } from '@zenstackhq/server/next';
import { RestApiHandler } from '@zenstackhq/server/api';
import { getPrisma } from '~/lib/db';
const handler = NextRequestHandler({
getPrisma,
useAppDir: true,
handler: RPCApiHandler() // you can also omit it since `RPCApiHandler` is the default
});
export {
handler as GET,
handler as POST,
handler as PUT,
handler as PATCH,
handler as DELETE,
};
import { SvelteKitHandler } from '@zenstackhq/server/sveltekit';
import { RPCApiHandler } from '@zenstackhq/server/api';
import { getPrisma } from './lib/db';
export const handle = SvelteKitHandler({
prefix: '/api/model',
handler: RPCApiHandler(), // you can also omit it since `RPCApiHandler` is the default
getPrisma
});
import { createEventHandler } from '@zenstackhq/server/nuxt';
import { RPCApiHandler } from '@zenstackhq/server/api';
import { getPrisma } from './lib/db';
export default createEventHandler({
handler: RPCApiHandler(), // you can also omit it since `RPCApiHandler` is the default
getPrisma
});
Wire Format
Input
For endpoints using GET
and DELETE
Http verbs, the query body is serialized and passed as the q
query parameter. E.g.:
GET /api/post/findMany?q=%7B%22where%22%3A%7B%22public%22%3Atrue%7D%7D
- Endpoint: /api/post/findMany
- Query parameters:
q
->{ "where" : { "public": true } }
For endpoints using other HTTP verbs, the query body is passed as application/json
in the request body. E.g.:
POST /api/post/create
{ "data": { "title": "Hello World" } }
Output
The output shape conforms to the data structure returned by the corresponding PrismaClient API, wrapped into a data
field. E.g.:
GET /api/post/findMany
{
"data": [ { "id": 1, "title": "Hello World" } ]
}
Serialization
This section explains the details about data serialization. If you're using generated hooks to consume the API, the generated code already automatically deals with serialization for you, and you don't need to do any further processing.
ZenStack uses superjson to serialize and deserialize data - including the q
query parameter, the request body, and the response body. Superjson generates two parts during serialization:
-
json:
The JSON-compatible serialization result.
-
meta:
The serialization metadata including information like field types that facilitates deserialization.
If the data only involves simple data types, the serialization result is the same as regular JSON.stringify
, and no meta
part is generated. However, for complex data types (like Bytes
, Decimal
, etc.), a meta
object will be generated, which needs to be carried along when sending the request, and will also be included in the response.
The following part explains how the meta
information is included for different situations:
-
The
q
query parameterIf during superjson-serialization of the
q
parameter, ameta
object is generated, it should be put into an object{ serialization: meta }
, JSON-stringified, and included as an additional query parametermeta
. For example, if you have a field namedbytes
ofBytes
type, and you may want to query with a filter like{ where: { bytes: Buffer.from([1,2,3]) } }
. Superjson-serializing the query object results in:{
"json": { "where": { "bytes": "AQID" } }, // base-64 encoded bytes
"meta": { "values": { "where.bytes": [["custom","Bytes"]] } }
}Your query URL should look like:
GET /api/post/findMany?q={"where":{"bytes":"AQID"}}&meta={"serialization":{"values":{"where.bytes":[["custom","Bytes"]]}}}
-
The request body
If during superjson-serialization of the request body, a
meta
object is generated, it should be put into an object{ serialization: meta }
, and included as an additional fieldmeta
field in the request body. For example, if you have a field namedbytes
ofBytes
type, and you may want to create a record with a value like{ data: { bytes: Buffer.from([1,2,3]) } }
. Superjson-serializing the request body results in:{
"json": { "bytes": "AQID" }, // base-64 encoded bytes
"meta": { "values": { "bytes": [[ "custom", "Bytes" ]] } }
}Your request body should look like:
POST /api/post/create
{
"data": { "bytes": "AQID" },
"meta": { "serialization": {"values": { "bytes": [[ "custom","Bytes" ]] } } }
} -
The response body
If during superjson-serialization of the response body, a
meta
object is generated, it will be put into an object{ serialization: meta }
, and included as an additional fieldmeta
field in the response body. For example, if you have a field namedbytes
ofBytes
type, and afindFirst
query returns{ id: 1, bytes: Buffer.from([1,2,3]) }
. Superjson-serializing the request body results in:{
"json": { "id": 1, "bytes":"AQID" }, // base-64 encoded bytes
"meta": { "values": { "bytes": [[ "custom", "Bytes" ]] } }
}Your response body will look like:
GET /api/post/findFirst
{
"data": { "id": 1, "bytes": "AQID" },
"meta": { "serialization": {"values": { "bytes": [[ "custom","Bytes"]] } } }
}You should use the meta.serialization field value to superjson-deserialize the response body.
Data Type Serialization Format
-
DateTime
ISO 8601 string
-
Bytes
Base64-encoded string
-
BigInt
String representation
-
Decimal
String representation
Endpoints
-
[model]/findMany
Http method:
GET
-
[model]/findUnique
Http method:
GET
-
[model]/findFirst
Http method:
GET
-
[model]/count
Http method:
GET
-
[model]/aggregate
Http method:
GET
-
[model]/groupBy
Http method:
GET
-
[model]/create
Http method:
POST
-
[model]/createMany
Http method:
POST
-
[model]/update
Http method:
PATCH
orPUT
-
[model]/updateMany
Http method:
PATCH
orPUT
-
[model]/upsert
Http method:
POST
-
[model]/delete
Http method:
DELETE
-
[model]/deleteMany
Http method:
DELETE
-
[model]/check
Http method:
GET
HTTP Status Code and Error Responses
Status code
The HTTP status code used by the endpoints follows the following rules:
create
andcreateMany
use201
for success. Other endpoints use200
.403
is used for to indicate the request is denied due to lack of permissions, usually caused by access policy violation.400
is used for invalid requests, e.g., malformed request body.422
is used for data validation errors.500
is used for other unexpected errors.
Error response format
{
// true to indicate the failure is due to a Prisma error
prisma?: boolean;
// true to indicate the failure is due to access policy violation
rejectedByPolicy?: boolean;
// original Prisma error code, available when `prisma` is true
code?: string;
// error message
message: string;
// extra reason about why a failure happened (e.g., 'RESULT_NOT_READABLE' indicates
// a mutation succeeded but the result cannot be read back due to access policy)
reason?: string;
}