10 votes
998 views
by Nikolas Burk

Introduction

In this tutorial, you will learn how to take an existing Prisma service and build a GraphQL server on top of it. The resolvers of the GraphQL servers will be connected to Prisma's GraphQL API via Prisma bindings.

The tutorial assumes that you already have a running Prisma API, so please make sure to have the endpoint for it available. If you don't have an endpoint, you can switch to this tutorial which starts entirely from scratch.

Why not use the Prisma API directly from your client applications?

One commonly asked question is why not to use Prisma's GraphQL API as your entire backend - it has a GraphQL API, so why bother writing another GraphQL server on top of it?

Prisma turns your database into a GraphQL API, exposing powerful CRUD operations to read and modify the data. This means letting your clients talk to Prisma directly is equivalent to directly exposing your entire database to your clients.

There are several reasons why this is not a suitable setup for production use cases:

  • Your clients should be able to consume a domain-specific API rather than working with generic CRUD operations
  • You want to provide authentication functionality for your users so that they can register with a password or some 3rd-party authentication provider
  • You want your API to integrate with microservices or other legacy systems
  • You want to include 3-rd party services (such as Stripe, GitHub, Yelp, ...) or other public APIs into your server
  • You don't want to expose your entire database schema to everyone (which would be the case due to GraphQL's introspection feature)

Update the data model

You already have your existing Prisma service, but for this tutorial we need to make sure that it has the right data model for the upcoming steps.

Note that we're assuming that your data model lives in a single file called datamodel.graphql. If that's not the case, please adjust your setup.

Step 1

Open datamodel.graphql and update its contents to look as follows:

type User {
  id: ID! @unique
  name: String!
  posts: [Post!]!
}

type Post {
  id: ID! @unique
  title: String!
  content: String!
  published: Boolean! @default(value: "false")
  author: User!
}
Step 2

After you saved the file, open your terminal and navigate into the root directory of your Prisma service (the one where prisma.yml is located) and run the following command to update its GraphQL API:

prisma deploy

The GraphQL API of your Prisma service now exposes CRUD operations for the User as well as the Post type that are defined in your data model and also lets you modify relations between them.

Setup the GraphQL server with graphql-yoga

Next, you'll create the file structure for your GraphQL server and add the required NPM dependencies to the project. Note that the directory that currently holds the files for your Prisma service (prisma.yml and datamodel.graphql) will later be located inside the GraphQL server directory.

Step 3

Navigate into a new directory and and the following commands in your terminal:

mkdir -p my-yoga-server/src
touch my-yoga-server/src/index.js
touch my-yoga-server/src/schema.graphql
cd my-yoga-server
yarn init -y

This creates the correct expected structure as well as a package.json file inside of it. index.js will be the entry-point of your server, schema.graphql defines the application schema (the GraphQL schema defining the GraphQL API of your server).

Step 4

Next, move the root directory of your Prisma service into my-yoga-server and rename it to prisma.

The structure of my-yoga-server should now look similar to this:

my-yoga-server
│
├── package.json
├── prisma
│   ├── datamodel.graphql
│   └── prisma.yml
└── src
    ├── schema.graphql
    └── index.js
Step 5

Next, install the required dependencies in your project:

yarn add graphql-yoga prisma-binding

Here's an overview of the two dependencies you added:

  • graphql-yoga: Provides the functionality for your GraphQL server (based on Express.js)
  • prisma-binding: Easily lets you connect your resolvers to Prisma's GraphQL API

Define the application schema

The API of every GraphQL server is defined by a corresponding GraphQL schema which specifies the API operations exposed by the server. It's now time to define the operations of your server's API.

As you might have guessed from the data model, you're building the API for a simple blogging application. As mentioned in the beginning, your clients should not be able to do whatever they like with the Post and User types, but instead will consume domain-specific operations which are tailored to their needs and the application domain.

Step 6

Open schema.graphql and add the following schema definition to it:

# import Post from './generated/prisma.graphql'
# import User from './generated/prisma.graphql'

type Query {
  posts(searchString: String): [Post!]!
  user(id: ID!): User
}

type Mutation {
  createDraft(authorId: ID!, title: String!, content: String!): Post
  publish(id: ID!): Post
  deletePost(id: ID!): Post
  signup(name: String!): User!
}

This GraphQL schema defines six operations (two queries and four mutations):

  • posts(searchString: String): Retrieve the list of all Posts and potentially filter them by providing a searchString
  • user(id: ID!): Retrieve a single User by its id
  • createDraft(authorId: ID!, title: String!, content: String!): Create a new draft (i.e. a Post where published is set to false) written by a specific User identified by authorId
  • publish(id: ID!): Publish a draft (i.e. set its published field to true)
  • deletePost(id: ID!): Delete a Post
  • signup(name: String!): Create a new User by providing a name

There's one odd thing about the GraphQL schema right now though: The Post and User types are imported from the src/generated/prisma.graphql which doesn't even exist yet - don't worry you'll download it next. Another thing to notice is that the import syntax uses GraphQL comments. These comments must not be deleted! They are used by graphql-import, a tool that lets you import SDL types across files which is not possible with standard SDL (yet!).

Download the Prisma database schema

The next step is to download the GraphQL schema of Prisma's GraphQL API (also referred to as Prisma database schema) into your project so you can properly import the SDL types from there.

Technically speaking, this is not absolutely necessary since you could also just redefine identical Post and User types in schema.graphql. However, this would mean that you now have two separate locations where these type definitions live. Whenever you now wanted to update the types, you'd have to do that twice. It is therefore considered best pratice to import the types from Prisma's GraphQL schema.

Downloading the Prisma database schema is done using the GraphQL CLI and GraphQL Config.

Step 7

Install the GraphQL CLI using the following command:

yarn global add graphql-cli
Step 8

Next, create your .graphqlconfig in the root directory of the server (i.e. in the my-yoga-server directory):

touch .graphqlconfig.yml
Step 9

Put the following contents into it, defining the two GraphQL APIs you're working with in this project (Prisma's GraphQL API as well as the customized API of your graphql-yoga server):

projects:
  app:
    schemaPath: src/schema.graphql
    extensions:
      endpoints:
        default: http://localhost:4000
  prisma:
    schemaPath: src/generated/prisma.graphql
    extensions:
      prisma: prisma/prisma.yml

The information you provide in this file is used by the GraphQL CLI as well as the GraphQL Playground. In the Playground specifically it allows you to work with both APIs side-by-side.

Step 10

To download the Prisma database schema to src/generated/prisma.graphql, run the following command in your terminal:

graphql get-schema --project prisma

The Prisma database schema which defines the full CRUD API for your database is now available in the location you specified in the projects.prisma.schemaPath property in your .graphqlconfig.yml (which is src/generated/prisma.graphql) and the import statements will work properly.

If you want the Prisma database schema to update automatically every time you deploy changes to your Prisma services (e.g. an update to the data model), you can add the following post-deployment hook to your prisma.yml file:

hooks:
  post-deploy:
    - graphql get-schema -p prisma

Instantiate the Prisma binding

The last step before implementing the resolvers for your application schema is to ensure these resolvers can access Prisma's GraphQL API via a Prisma binding.

You'll therefore instantiate a Prisma binding and attach to the context object which is passed through the resolver chain.

Step 11

Open index.js and add the following code to it - note that you need to replace the __YOUR_PRISMA_ENDPOINT__ placeholder with the endpoint of your Prisma API (which you can find in prisma.yml):

const { GraphQLServer } = require('graphql-yoga')
const { Prisma } = require('prisma-binding')

const resolvers = {
  Query: {
    posts: (_, args, context, info) => {
      // ...
    },
    user: (_, args, context, info) => {
      // ...
    }
  },
  Mutation: {
    createDraft: (_, args, context, info) => {
      // ...
    },
    publish: (_, args, context, info) => {
      // ...
    },
    deletePost: (_, args, context, info) => {
      // ...
    },
    signup: (_, args, context, info) => {
      // ...
    }
  }
}

const server = new GraphQLServer({
  typeDefs: 'src/schema.graphql',
  resolvers,
  context: req => ({
    ...req,
    prisma: new Prisma({
      typeDefs: 'src/generated/prisma.graphql',
      endpoint: '__YOUR_PRISMA_ENDPOINT__',
    }),
  }),
})
server.start(() => console.log(`GraphQL server is running on http://localhost:4000`))

You will add the actual implementation for the resolvers in a bit. Again, be sure to replace the __YOUR_PRISMA_ENDPOINT__ placeholder with the endpoint of your Prisma service. This endpoint is specified as the endpoint property in your prisma.yml.

Implement the resolvers for your GraphQL server using Prisma bindings

By attaching the Prisma binding instance to the context, all your resolver functions get access to it and can invoke its binding functions. Instead of the resolvers needing to access a database directly, they're effectively delegating the execution of the incoming queries to Prisma's GraphQL API.

Bindings functions provide a convenient API to send queries and mutations to a GraphQL API using JavaScript. The neat thing is that you can pass along the info object which contains the information about which fields a client requested in its query - that way, Prisma can resolve the query very efficiently.

Step 12

Inside index.js, add implementations for the resolver functions like so:

const resolvers = {
  Query: {
    posts: (_, args, context, info) => {
      return context.prisma.query.posts(
        {
          where: {
            OR: [
              { title_contains: args.searchString },
              { content_contains: args.searchString },
            ],
          },
        },
        info,
      )
    },
    user: (_, args, context, info) => {
      return context.prisma.query.user(
        {
          where: {
            id: args.id,
          },
        },
        info,
      )
    },
  },
  Mutation: {
    createDraft: (_, args, context, info) => {
      return context.prisma.mutation.createPost(
        {
          data: {
            title: args.title,
            content: args.content,
            author: {
              connect: {
                id: args.authorId,
              },
            },
          },
        },
        info,
      )
    },
    publish: (_, args, context, info) => {
      return context.prisma.mutation.updatePost(
        {
          where: {
            id: args.id,
          },
          data: {
            published: true,
          },
        },
        info,
      )
    },
    deletePost: (_, args, context, info) => {
      return context.prisma.mutation.deletePost(
        {
          where: {
            id: args.id,
          },
        },
        info,
      )
    },
    signup: (_, args, context, info) => {
      return context.prisma.mutation.createUser(
        {
          data: {
            name: args.name,
          },
        },
        info,
      )
    },
  },
}

The implementation of each resolver simply is an invocation of a binding function to delegate the operation down to the Prisma API, saving you from having to perform any manual database access.

Start the server and use the API in a Playground

The implementation of your GraphQL server is now done and you can work with it inside a GraphQL Playground.

Step 13

In your terminal, start the server using the following command:

node src/index.js

Now, you can open a GraphQL Playground either by navigating to http://localhost:4000 in your browser.

Instead of directly accessing the URL in your browser, you can also use the graphql playground command which will read the information from your .graphqlconfig.yml file and lets you work with both GraphQL APIs side-by-side.

To test the API, you can send the following queries and mutations:

Create a new user:

mutation {
  signup(name: "Alice") {
    id
  }
}

Create a new draft for a User:

Note that you need to replace the __USER_ID__ placeholder with the id of an actual User from your database.

mutation {
  createDraft(
    title: "Join us at GraphQL Europe 🇪🇺 ",
    content: "Get a 10%-discount with this promo code on graphql-europe.org: gql-boilerplates",
    authorId: "__USER_ID__"
  ) {
    id
    published
  }
}

Publish a draft:

Note that you need to replace the __POST_ID__ placeholder with the id of an actual Post from your database.

mutation {
  publish(
    id: "__POST_ID__",
  ) {
    id
    published
  }
}

Filter posts for "GraphQL Europe"

query {
  posts(searchString: "GraphQL Europe") {
    id
    title
    content
    published
    author {
      id
      name
    }
  }
}

Delete a post:

Note that you need to replace the __POST_ID__ placeholder with the id of an actual Post from your database.

mutation {
  deletePost(
    id: "__POST_ID__",
  ) {
    id
  }
}