Overview
What are Prisma bindings?
Prisma bindings are GraphQL bindings for the GraphQL APIs exposed by Prisma services.
This page introduces the idea of Prisma bindings and explains how they can be used to build GraphQL servers. To learn more about the API of Prisma bindings, visit the documentation for GraphQL bindings here.
To learn more about the differences between Prisma bindings and the Prisma client, you can read this forum post.
The idea of GraphQL bindings
GraphQL bindings are minimal, auto-generated and schema-specific GraphQL clients. A GraphQL binding lets you talk to a GraphQL API by invoking auto-generated binding functions rather than manually constructing and sending HTTP requests.
Consider the following GraphQL schema definition as an example:
type User {
id: ID!
name: String!
}
type Query {
users: [User!]!
}
type Mutation {
createUser(name: String!): User!
}
The API defined by this schema accepts two operations, the users
query and the createUser
mutation.
Consequently, a GraphQL binding for this API exposes exactly two functions named after the root fields of the schema:
binding.query.users(args, selectionSet)
is generated based on theusers
root fieldbinding.mutation.createUser(args, selectionSet)
is generated based on thecreateUser
root field
Both functions are auto-generated and when invoked, they construct and send the respective query/mutation to the API.
Bindings functions construct and send operations to a GraphQL API
But how does the binding know what fields to include in the selection set of the query it constructs and sends to the API?
Same question for the name
argument of the createUser
mutation, how does the auto-generated binding function know what parameter values to provide when constructing the actual mutation?
That's exactly what the two arguments of the generated binding functions are for:
args
is an object carrying any parameters for the operation.selectionSet
is a string specifying the selection set of the operation, i.e. what fields should be included in the returned JSON response (this can also be provided as an object).
Let's now understand how the users
binding function can be used to construct and send the following query to the GraphQL API:
query {
users {
id
name
}
}
To achieve this, you need to invoke the users
function as follows:
binding.query.users({}, `{ id name }`)
The first argument passed to users
is an empty object (because the users
root field in the schema definition above doesn't accept any parameters).
The second argument is the selection set - to achieve the same result as the sample query above, we need to include id
and the name
.
The selection set passed as the second argument actually is a GraphQL fragment (in this case on the User
type). Binding functions however accept a shorthand notation that omits the fragment
keyword as well the fragment name. So instead of:
fragment UserFragment on User {
id
name
}
you can simply write:
{ id name }
New lines are generally ignored in GraphQL.
Let's now go through the same exercise for createUser
and think about how we can send the following mutation using one of the binding functions:
mutation {
createUser(name: "Sarah") {
id
}
}
This mutation can be sent using the createUser
binding function:
binding.mutation.createUser({ name: 'Sarah' }, `{ id }`)
This time, we're providing an object that contains the values for the parameters of the mutation as the first argument. In this case, there's only the name
to be provided.
Similar to the users
query, we're also passing the selection set (written again as a shorthand GraphQL fragment) as the second argument. Because the selection set in the sample mutation above only includes the id
, that's what we're doing in the function call as well.
GraphQL bindings: An auto-generated SDK for your GraphQL API
Another way of looking at GraphQL bindings is as auto-generated SDKs for GraphQL APIs. An SDK is a library that exposes a set of convenience functions for you to interact with a (web) API. Instead of talking to the API directly, you can use the utilities provided by the SDK which reduces boilerplate and (ideally) improves the developer ergonomics of communicating with the API.
GraphQL bindings effectively serve the same purpose. The binding functions are equivalent to the root fields of the corresponding GraphQL schema and allow to interact with the GraphQL API in a more convenient way.
Prisma bindings have additional Prisma-specific features
Prisma bindings are a superset of GraphQL bindings, they're an extended version of GraphQL bindings specifically for Prisma APIs.
There are two extra features in Prisma bindings (compared to regular GraphQL bindings):
- baked in authentication using the service secret
- the
exists
function lets you to check whether a certain element exists in the database
Prisma bindings use the data loader pattern to batch requests to the Prisma API.
Baked in authentication
The service secret is passed to the constructor of the Prisma
binding. This allows the binding to generate the required service token (JWT) when it sends requests to your service's API.
Consider the following example:
const Prisma = require('prisma-binding')
const prisma = new Prisma({
typeDefs: 'prisma.graphql',
endpoint: 'http://localhost:4466/myservice/dev',
secret: 'mysecret42',
})
The prisma
binding object can now be used to send requests directly to your service by invoking the auto-generated binding functions. Each request will be authenticated with a service token generated based on the secret
that was passed to the Prisma
constructor.
Checking for existence with exists
As mentioned, the exists
function allows to easily check whether a certain element exists in the database. You can provide filter criteria to the function call and it will return a boolean value indicating whether (at least) one element meeting those criteria exists. As an example, say a Prisma service was based on the following data model:
type Post {
id: ID! @unique
title: String!
}
For each type in your data model, the Prisma
binding creates a dedicated exists
function that is named after the type. Here are a few examples demonstrating how exists
can be used:
// Check whether a `Post` with a specific `id` exists
prisma.exists.Post({ id: "cji4iv4ucwc1c0a9671z7t8pt" })
// Check whether a `Post` exists whose `id` is in the provided list
prisma.exists.Post({ id_in: ["cji4iv4ucwc1c0a9671z7t8pt", "cji4k3j9w29yq0a96xp5fufj1", "cji4j2g0lxis40a9604a80kmx"] })
// Check whether a `Post` exists that has `I like GraphQL` as `title`
prisma.exists.Post({ title: "I like GraphQL" })
// Check whether a `Post` exists where the `title`contains the string `GraphQL`
prisma.exists.Post({ title_contains: "GraphQL" })`
Schema delegation using the info
object
Prisma bindings can be used in two modes:
- Provide the selection set as a string (similar to all examples you've seen on this page)
- Provide the selection set as the
info
object inside a resolver function
The second mode is referred to as schema delegation. Schema delegation only works when there is an info
object available and therefore the only context where schema delegation is possible is inside of resolver functions.
The
info
object is the fourth argument passed into a resolver function. It carries the AST and other useful information about the query that's currently being resolved, e.g. what parameter values were provided. Learn more about it here.
The idea of schema delegation is that a GraphQL query that's received by GraphQL server A
is not resolved by A
's execution engine. Instead, A
delegates the query to GraphQL server B
whose GraphQL execution engine will finally take care of resolving the query.
In this example, the feed
resolver in the application server delegates the resolution of the incoming query to the posts
resolver of the Prisma API. Note that this only works when the respective fields of both resolvers have the same type.
The info
object passed into the feed
resolver carries the AST of the initial feed
query. This AST contains information about the query's selection set, in this case it contains the id
and title
fields. The binding function extracts this information from the info
object and therefore is able to construct a corresponding query which it sends to Prisma.
Building GraphQL servers with Prisma bindings
Database layer vs Application layer
Before discussing how to implement your resolvers using Prisma bindings and schema delegation, let's understand the general architecture of your GraphQL server.
The most important part to understand is that when building GraphQL servers with Prisma, you're dealing with two(!) GraphQL APIs:
- The database layer is provided by Prisma's CRUD/realtime GraphQL API. The GraphQL schema defining this API is the auto-generated Prisma GraphQL schema.
- The application layer is responsible for any functionality that's not directly related to writing or reading data from the database (like business logic, authentication and permissions, 3rd-party integrations,...). When working with Node.JS, the application layer is commonly implemented with the
graphql-yoga
library. The GraphQL schema defining the API of the application layer is called application schema.
The application layer is where you'll spend most of your actual coding work. Here you need to define the GraphQL schema for the API your client applications will talk to and implement the corresponding resolver functions (these resolvers will be delegating requests to Prisma).
The layered architectural approach is best practice in modern application development, separating business logic from database access. It is similar to architectures used by companies like Twitter or Facebook where a dedicated data access layer abstracts away the underlying database and provides a data access API consumed by the application layer.
The core benefit of this layered architectural approach is a clear separation of concerns. Removing the complexity of database access and putting it into its own layer lets application developers focus on implementing business logic and value-adding features. The database layer takes care of various concerns related to database access, such as security, synchronization, query optimization and other performance-related tasks.
Bridging application and database layer: An example
When building a GraphQL server with Prisma, these are the high-level steps you need to perform:
Configure database layer
- Specify your data model for your Prisma service in SDL
- Deploy a Prisma service based on that data model
Implement application layer
- Write the application schema (i.e. define the operations of your API)
- Implement resolver functions for the API operations (using Prisma bindings)
- Configure and start the GraphQL server with application schema and resolvers
Let's get concrete and walk through an example.
1.1. Specify data model for Prisma service
The first step is to define the data model for a Prisma service, here is the one for this example:
type Post {
id: ID! @unique
title: String!
content: String!
published: Boolean! @default(value: "false")
}
1.2. Deploy a Prisma service
To deploy the Prisma service, we also need a minimal prisma.yml. Assumed the above data model is stored in a file called datamodel.prisma
, this is what prisma.yml could look like:
endpoint: http://localhost:4466
datatamodel: datamodel.prisma
Now you can run prisma deploy
and the service will be deployed to a Prisma server running on http://localhost:4466
.
The Prisma GraphQL schema for the generated Prisma API looks similar to this (this is a shortened version, find the full version here):
type Post implements Node {
id: ID!
title: String!
content: String!
published: Boolean!
}
type Mutation {
createPost(data: PostCreateInput!): Post!
updatePost(data: PostUpdateInput!, where: PostWhereUniqueInput!): Post
deletePost(where: PostWhereUniqueInput!): Post
}
type Query {
posts(
where: PostWhereInput
orderBy: PostOrderByInput
skip: Int
after: String
before: String
first: Int
last: Int
): [Post]!
post(where: PostWhereUniqueInput!): Post
}
type Subscription {
post(where: PostSubscriptionWhereInput): PostSubscriptionPayload
}
The API defined by this schema can now be accessed on http://localhost:4466
.
2.1. Write application schema
In the following it is assumed that the Prisma GraphQL schema is available to the application layer as a file called
prisma.graphql
(it might have been downloaded via a post-deployment hook).
To write the application schema, you need to think about the API operations that should be exposed to your clients. This is your chance to transform Prisma's generic CRUD operations into a domain-specific API suited to your clients' needs.
For example, if you wanted to build a blogging app, the following GraphQL schema would already encode some domain rules specific to your app:
# import Post from "prisma.graphql"
type Query {
feed: [Post!]!
findPostById(id: ID!): Post
}
type Mutation {
createDraft(title: String!, content: String!): Post!
publish(id: ID!): Post
deletePost(id: ID!): Post
}
The main point of creating this application schema is to narrow down the scope of possible actions for your clients. Instead of getting full CRUD access, clients are only able to perform operations following your business rules.
Here's a quick run-through of the available operations (note that Post
elements where published
is false
are called drafts):
feed
: Returns all published postsfindPostById
: Returns any post by its idcreateDraft
: Create a new draftpublish
: Publish a draft (it will then be returned byfeed
)deletePost
: Delete any post by its id
2.2. Implement resolver functions
Each of the five root fields in the above application schema needs to have a corresponding resolver function. Here is what the implementation of these resolvers is going to look like:
const resolvers = {
Query: {
feed: (parent, args, context, info) => {
return context.db.query.posts({ where: { published: true } }, info)
},
findPostById: (parent, args, context, info) => {
return context.db.query.post({ where: { id: args.id } }, info)
},
},
Mutation: {
createDraft: (parent, args, context, info) => {
return context.db.mutation.createPost(
{
data: {
title: args.title,
published: false,
},
},
info,
)
},
publish: (parent, args, context, info) => {
return context.db.mutation.updatePost(
{
where: { id: args.id },
data: { published: true },
},
info,
)
},
deletePost: (parent, args, context, info) => {
return context.db.mutation.deletePost({ where: { id: args.id } }, info)
},
},
}
All resolver follow a similar pattern in that it uses the Prisma binding object (called db
) that's attached to the context
. The binding object exposes the operations of the Prisma API.
All a resolver needs to do is invoke the correct binding function and pass on the info
object. The application layer will never talk to the database directly but only through the Prisma API.
2.3. Configure and start the GraphQL server
const GraphQLSever = require('graphql-yoga')
const Prisma = require('prisma-binding')
const resolvers = require('./resolvers')
const server = new GraphQLServer({
typeDefs: './schema.graphql'
resolvers,
context: {
db: new Prisma({
typeDefs: './prisma.graphql',
endpoint: 'http://localhost:4466'
})
}
})
server.start(() => console.log(`Sever is running on http://localhost:4000`))
In this configuration, we assume that the schema definition from 2.1. and the resolver implementation from 2.2. are available in files called schema.graphql
and resolvers.js
. They're both passed to the constructor of the GraphQLServer
.
The Prisma
binding instance is further attached to the context
of the GraphQLServer
. This is the reason why the resolvers are able to invoke Prisma binding functions, because they get access to the binding object through the context
object.
If you want to explore this particular example, check out the corresponding step-by-step tutorial.
Lifecycle of a GraphQL query
The goal for this section is to understand the end-to-end flow for sending a GraphQL query to your application server and receiving the response, including its path through Prisma.
Step 1: The client sends a GraphQL query to the application server
The findPostById
query from the application schema is used as an example. Assume a client sends the following query to your application server:
query {
findPostById(id: "cji4iv4ucwc1c0a9671z7t8pt") {
id
title
published
}
}
Step 2: The application server receives the GraphQL query
A GraphQL server resolves a query by invoking the resolver functions for the fields contained in the query. The first field in the incoming query is findPostById
, so the findPostById
resolver is the first one to be invoked. As a reminder, this is what the findPostById
resolver looks like:
findPostById: (parent, args, context, info) => {
return context.db.query.post({ where: { id: args.id } }, info)
},
Step 3: The Prisma binding to constructs a query for the Prisma API
The findPostById
resolver only has one line of code: return context.db.query.post({ where: { id: args.id } }, info)
.
Invoking the post
binding function means that a post
query will be constructed:
query {
post(???) {
???
}
}
To know which parameters the binding function injects into the query, we need to look at the first argument passed into the binding function. In this case it is an object looking as follows: { where: { id: args.id } }
. The value of args.id
is cji4iv4ucwc1c0a9671z7t8pt
. Consequently, the binding functions injects the following query arguments:
query {
post(where: {
id: "cji4iv4ucwc1c0a9671z7t8pt"
}) {
???
}
}
Finally, the binding function needs to know the selection set for the query. That's the purpose of the second argument passed into it. The info
object contains the AST of the initial feed
query. The binding function now extracts the selection set from that query and injects it into the query for the Prisma API:
query {
post(where: { id: "cji4iv4ucwc1c0a9671z7t8pt" }) {
id
title
published
}
}
Step 4: The binding function sends the constructed query to the Prisma API
The post
query is now sent to the Prisma service where it's being resolved. Prisma generates a corresponding database query and retrieves the requested data from the database.
Step 5: The Prisma API returns the requested data
Assume the Prisma API responds with the following data:
{
"data": {
"post": {
"id": "cji4iv4ucwc1c0a9671z7t8pt",
"title": "GraphQL is cool",
"published": true
}
}
}
Step 6: The binding function receives the response from the Prisma API
The post
binding function receives the payload of the service's response. It strips off the "data"
key from the JSON object and returns the following post
object:
{
"post": {
"id": "cji4iv4ucwc1c0a9671z7t8pt",
"title": "GraphQL is cool",
"published": true
}
}
Step 7: The post
object is returned by the findPostById
resolver
The findPostById
resolver is next in the server's callstack. It returns the post
object that just came out of the post
binding function, again including the id
, title
and published
properties.
Step 8: The application server returns the response
Before the HTTP response is returned, the GraphQL execution engine wraps the post
object (that was returned by the findPostbyId
resolver) with the "data"
key (this is required by the GraphQL specification):
{
"data": {
"post": {
"id": "cji4iv4ucwc1c0a9671z7t8pt",
"title": "GraphQL is cool",
"published": true
}
}
}