RESTful API Handler
Introduction
The RESTful-style API handler exposes CRUD APIs as RESTful endpoints using JSON:API as transportation format. 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 as the following:
- Next.js
- SvelteKit
- Nuxt
import { NextRequestHandler } from '@zenstackhq/server/next';
import RestApiHandler from '@zenstackhq/server/api/rest';
import { getPrisma } from '../../lib/db';
export default NextRequestHandler({
getPrisma,
handler: RestApiHandler({ endpoint: 'http://myhost/api' })
});
import zenstack from '@zenstackhq/server/sveltekit';
import RestApiHandler from '@zenstackhq/server/api/rest';
import { getPrisma } from './lib/db';
export const handle = zenstack.SvelteKitHandler({
prefix: '/api/model',
getPrisma,
handler: RestApiHandler({ endpoint: 'http://myhost/api/model' })
});
🚧 Coming soon
The factory function accepts an options object with the following fields:
-
endpoint
Required. A
string
field representing the base URL of the RESTful API, used for generating resource links. -
pageSize
Optional. A
number
field representing the default page size for listing resources and relationships. Defaults to 100. Set to Infinity to disable pagination.
Endpoints and Features
The RESTful API handler conforms to the the JSON:API v1.1 specification for its URL design and input/output format. The following sections list the endpoints and features are implemented. The examples refer to the following schema modeling a blogging app:
model User {
id Int @id @default(autoincrement())
email String
posts Post[]
}
model Profile {
id Int @id @default(autoincrement())
gender String
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(false)
viewCount Int @default(0)
author User @relation(fields: [authorId], references: [id])
authorId Int
comments Comment[]
}
model Comment {
id Int @id @default(autoincrement())
content String
post Post @relation(fields: [postId], references: [id])
postId Int
}
Listing resources
A specific type of resource can be listed using the following endpoint:
GET /:type
Status codes
- 200: The request was successful and the response body contains the requested resources.
- 400: The request was malformed.
- 403: The request was forbidden.
- 404: The requested resource type does not exist.
Examples
GET /post
{
"meta": {
"total": 1
},
"data": [
{
"attributes": {
"authorId": 1,
"published": true,
"title": "My Awesome Post",
"viewCount": 0
},
"id": 1,
"links": {
"self": "http://myhost/api/post/1"
},
"relationships": {
"author": {
"data": { "id": 1, "type": "user" },
"links": {
"related": "http://myhost/api/post/1/author/1",
"self": "http://myhost/api/post/1/relationships/author/1"
}
}
},
"type": "post"
}
],
"jsonapi": {
"version": "1.1"
},
"links": {
"first": "http://myhost/api/post?page%5Blimit%5D=100",
"last": "http://myhost/api/post?page%5Boffset%5D=0",
"next": null,
"prev": null,
"self": "http://myhost/api/post"
}
}
Fetching a resource
A unique resource can be fetched using the following endpoint:
GET /:type/:id
Status codes
- 200: The request was successful and the response body contains the requested resource.
- 400: The request was malformed.
- 403: The request was forbidden.
- 404: The requested resource type or ID does not exist.
Examples
GET /post/1
{
"data": {
"attributes": {
"authorId": 1,
"published": true,
"title": "My Awesome Post",
"viewCount": 0
},
"id": 1,
"links": {
"self": "http://myhost/api/post/1"
},
"relationships": {
"author": {
"data": { "id": 1, "type": "user" },
"links": {
"related": "http://myhost/api/post/1/author/1",
"self": "http://myhost/api/post/1/relationships/author/1"
}
}
},
"type": "post"
},
"jsonapi": {
"version": "1.1"
},
"links": {
"self": "http://myhost/api/post/1"
}
}
Fetching relationships
A resource's relationships can be fetched using the following endpoint:
GET /:type/:id/relationships/:relationship
Status codes
- 200: The request was successful and the response body contains the requested relationships.
- 400: The request was malformed.
- 403: The request was forbidden.
- 404: The requested resource type, ID, or relationship does not exist.
Examples
-
Fetching a to-one relationship
GET /post/1/relationships/author
{
"data" : { "id" : 1, "type" : "user" },
"jsonapi" : {
"version" : "1.1"
},
"links" : {
"self" : "http://myhost/api/post/1/relationships/author"
}
} -
Fetching a to-many relationship
GET /user/1/relationships/posts
{
"data" : [
{ "id" : 1, "type" : "post" },
{ "id" : 2, "type" : "post" }
],
"jsonapi" : {
"version" : "1.1"
},
"links" : {
"first" : "http://myhost/api/user/1/relationships/posts?page%5Blimit%5D=100",
"last" : "http://myhost/api/user/1/relationships/posts?page%5Boffset%5D=0",
"next" : null,
"prev" : null,
"self" : "http://myhost/api/user/1/relationships/posts"
}
}
Fetching related resources
GET /:type/:id/:relationship
Status codes
- 200: The request was successful and the response body contains the requested relationship.
- 400: The request was malformed.
- 403: The request was forbidden.
- 404: The requested resource type, ID, or relationship does not exist.
Examples
GET /post/1/author
{
"data" : {
"attributes" : {
"email" : "emily@zenstack.dev",
"name" : "Emily"
},
"id" : 1,
"links" : {
"self" : "http://myhost/api/user/1"
},
"relationships" : {
"posts" : {
"links" : {
"related" : "http://myhost/api/user/1/posts",
"self" : "http://myhost/api/user/1/relationships/posts"
}
}
},
"type" : "user"
},
"jsonapi" : {
"version" : "1.1"
},
"links" : {
"self" : "http://myhost/api/post/1/author"
}
}
Fine-grained data fetching
Filtering
You can use the filter[:selector1][:selector2][...]=value
query parameter family to filter resource collections or relationship collections.
Examples
-
Equality filter against plain field
GET /api/post?filter[published]=false
-
Equality filter against relationship
Relationship field can be filtered directly by its id.
GET /api/post?filter[author]=1
If the relationship is to-many, the filter has "some" semantic and evaluates to
true
if any of the items in the relationship matches.GET /api/user?filter[posts]=1
-
Filtering with multiple values
Multiple filter values can be separated by comma. Items statisfying any of the values will be returned.
GET /api/post?filter[author]=1,2
-
Multiple filters
A request can carry multiple filters. Only items statisfying all filters will be returned.
GET /api/post?filter[author]=1&filter[published]=true
-
Deep filtering
A filter can carry multiple field selectors to reach into relationships.
GET /api/post?filter[author][name]=Emily
When reaching into a to-many relationship, the filter has "some" semantic and evaluates to
true
if any of the items in the relationship matches.GET /api/user?filter[posts][published]=true
-
Filtering with comparison operators
Filters can go beyond equality by appending an "operator suffix".
GET /api/post?filter[viewCount$gt]=100
The following operators are supported:
-
$lt
Less than
-
$lte
Less than or equal to
-
$gt
Greater than
-
$gte
Greater than or equal to
-
$contains
String contains
-
$icontains
Case-insensitive string contains
-
$search
String full-text search
-
$startsWith
String starts with
-
$endsWith
String ends with
-
$has
Collection has value
-
$hasEvery
Collection has every element in value
-
$hasSome
Collection has some elements in value
-
$isEmpty
Collection is empty
-
Sorting
You can use the sort
query parameter to sort resource collections or relationship collections. The value of the parameter is a comma-separated list of fields names. The order of the fields in the list determines the order of sorting. By default, sorting is done in ascending order. To sort in descending order, prefix the field name with a minus sign.
Examples
GET /api/post?sort=createdAt,-viewCount
Pagination
When creating a RESTful API handler, you can pass in a pageSize
option to control pagination behavior of fetching a collection of resources, related resources, and relationships. By default the page size is 100, and you can disable pagination by setting pageSize
option to Infinity
.
When fetching a collection resource or relationship, you can use the page[offset]=value
and page[limit]=value
query parameter family to fetch a specific page. They're mapped to skip
and take
parameters in the query arguments sent to PrismaClient.
The response data of collection fetching contains pagination links that facilitate navigating through the collection. The "meta" section also contains the total count available. E.g.:
{
"meta": {
"total": 10
},
"data" : [
...
],
"links" : {
"first" : "http://myhost/api/post?page%5Blimit%5D=2",
"last" : "http://myhost/api/post?page%5Boffset%5D=4",
"next" : "http://myhost/api/post?page%5Boffset%5D=4&page%5Blimit%5D=2",
"prev" : "http://myhost/api/post?page%5Boffset%5D=0&page%5Blimit%5D=2",
"self" : "http://myhost/api/post"
}
}
Examples
-
Fetching a specific page of resources
GET /api/post?page[offset]=10&page[limit]=5
-
Fetching a specific page of relationships
GET /api/user/1/relationships/posts?page[offset]=10&page[limit]=5
-
Fetching a specific page of related resources
GET /api/user/1/posts?page[offset]=10&page[limit]=5
Including related resources
You can use the include
query parameter to include related resources in the response. The value of the parameter is a comma-separated list of fields names. Field names can contain dots to reach into nested relationships.
When including related resources, the response data takes the form of Compound Documents and contains a included
field carrying normalized related resources. E.g.:
{
"data" : [
{
"attributes" : {
...
},
"id" : 1,
"relationships" : {
"author" : {
"data" : { "id" : 1, "type" : "user" }
}
},
"type" : "post"
}
],
"included" : [
{
"attributes" : {
"email" : "emily@zenstack.dev",
"name" : "Emily"
},
"id" : 1,
"links" : {
"self" : "http://myhost/api/user/1"
},
"relationships" : {
"posts" : {
"links" : {
"related" : "http://myhost/api/user/1/posts",
"self" : "http://myhost/api/user/1/relationships/posts"
}
}
},
"type" : "user"
}
]
}
Examples
-
Including a direct relationship
GET /api/post?include=author
-
Including a deep relationship
GET /api/post?include=author.profile
-
Including multiple relationships
GET /api/post?include=author,comments
Creating a resource
A new resource can be created using the following endpoint:
POST /:type
Status codes
- 201: The request was successful and the resource was created.
- 400: The request was malformed.
- 403: The request was forbidden.
- 404: The requested resource type does not exist.
Examples
-
Creating a resource
POST /user
{
"data": {
"type": "user",
"attributes": {
"name": "Emily",
"email": "emily@zenstack.dev"
}
}
} -
Creating a resource with relationships attached
POST /user
{
"data": {
"type": "user",
"attributes": {
"name": "Emily",
"email": "emily@zenstack.dev"
},
"relationships": {
"posts": {
"data": [{ "type": "post", "id": 1 }]
}
}
}
}
Updating a resource
A resource can be updated using the following endpoints:
PUT /:type/:id
PATCH /:type/:id
Both PUT
and PATCH
do partial update and has exactly the same behavior.
Besides plain fields, you can also include relationships in the request body. Please note that this won't update the related resource; instead if only replaces the relationships. If you update a to-many relationship, the new collection will entirely replace the old one.
Relationships can also be manipulated directly. See Manipulating Relationships for more details.
Status codes
- 200: The request was successful and the resource was updated.
- 400: The request was malformed.
- 403: The request was forbidden.
- 404: The requested resource type or ID does not exist.
Examples
-
Updating a resource
PUT /post/1
{
"data": {
"type": "post",
"attributes": {
"title": "My Awesome Post"
}
}
} -
Updating a resource's relationships
PUT /user/1
{
"data": {
"type": "user",
"relationships": {
"posts": {
"data": [{ "type": "post", "id": 2 }]
}
}
}
}
Deleting a resource
A resource can be deleted using the following endpoint:
Status codes
- 204: The request was successful and the resource was deleted.
- 403: The request was forbidden.
- 404: The requested resource type or ID does not exist.
DELETE /:type/:id
Manipulating relationships
Relationships can be manipulated using the following endpoints:
Adding to a to-many relationship
POST /:type/:id/relationships/:relationship
Status codes
- 200: The request was successful and the relationship was updated.
- 403: The request was forbidden.
- 404: The requested resource type, ID, or relationship does not exist.
Examples
POST /user/1/relationships/posts
{
"data": [
{ "type": "post", "id": "1" },
{ "type": "post", "id": "2" }
]
}
Updating a relationship (to-one or to-many)
PUT /:type/:id/relationships/:relationship
PATCH /:type/:id/relationships/:relationship
PUT
and PATCH
has exactly the same behavior and both relace the existing relationships with the new ones entirely.
Status codes
- 200: The request was successful and the relationship was updated.
- 403: The request was forbidden.
- 404: The requested resource type, ID, or relationship does not exist.
Examples
-
Replacing a to-many relationship
PUT /user/1/relationships/posts
{
"data": [
{ "type": "post", "id": "1" },
{ "type": "post", "id": "2" }
]
} -
Replacing a to-one relationship
PUT /post/1/author
{
"data": { "type": "user", "id": "2" }
} -
Clearing a to-many relationship
PUT /user/1/relationships/posts
{
"data": []
} -
Clearing a to-one relationship
PUT /post/1/author
{
"data": null
}
Serialization
ZenStack uses superjson to serialize and deserialize data. 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.
When sending requests, if superjson-serializing the request body results in a meta
object, it should be put into a { "serialization": meta }
object and included in the meta
field of the request body. For example, if you have a bytes
field of type Bytes
, the request body should look like:
POST /post
{
"data": {
"type": "post",
"attributes": {
...
"bytes": "AQID" // base64-encoded bytes
}
},
"meta": {
"serialization": {"values": { "data.attributes.bytes": [[ "custom", "Bytes"]] } }
}
}
Correspondingly, the response body of a query may look like:
GET /post/1
{
"data": {
"id": "1",
"type": "post",
"attributes": {
...
"bytes": "AQID" // base64-encoded bytes
}
},
"meta": {
"serialization": {"values": { "data.attributes.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
Error Handling
An error response is an object containing the following fields:
-
errors
An array of error objects, each containing the following fields:
- code:
string
, error code - status:
number
, HTTP status code - title:
string
, error title - detail:
string
, error detail - prismaCode:
string
, Prisma error code, if the error is thrown by Prisma
- code:
Example
{
"errors" : [
{
"code" : "unsupported-model",
"detail" : "Model foo doesn't exist",
"status" : 404,
"title" : "Unsupported model type"
}
]
}