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.