Prisma GraphQL APIPrisma Bindings

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 the users root field
  • binding.mutation.createUser(args, selectionSet) is generated based on the createUser 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:

  1. Configure database layer

    1. Specify your data model for your Prisma service in SDL
    2. Deploy a Prisma service based on that data model
  2. Implement application layer

    1. Write the application schema (i.e. define the operations of your API)
    2. Implement resolver functions for the API operations (using Prisma bindings)
    3. 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 posts
  • findPostById: Returns any post by its id
  • createDraft: Create a new draft
  • publish: Publish a draft (it will then be returned by feed)
  • 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
    }
  }
}