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';
import { getPrisma } from '~/lib/db';
const handler = NextRequestHandler({
getPrisma,
useAppDir: true,
handler: RestApiHandler({ endpoint: 'http://myhost/api' })
});
export {
handler as GET,
handler as POST,
handler as PUT,
handler as PATCH,
handler as DELETE,
};
import { SvelteKitHandler } from '@zenstackhq/server/sveltekit';
import { RestApiHandler } from '@zenstackhq/server/api';
import { getPrisma } from './lib/db';
export const handle = SvelteKitHandler({
prefix: '/api/model',
getPrisma,
handler: RestApiHandler({ endpoint: 'http://myhost/api/model' })
});
import { createEventHandler } from '@zenstackhq/server/nuxt';
import { RestApiHandler } from '@zenstackhq/server/api';
import { getPrisma } from './lib/db';
export default createEventHandler({
handler: RestApiHandler({ endpoint: 'http://myhost/api/model' })
getPrisma
});
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.
- 422: The request violated data validation rules.
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