Overview

This upgrade guide describes how to migrate a Node.js project that's based on Prisma 1 and uses prisma-binding to implement a GraphQL server.

The code will keep the SDL-first approach for constructing the GraphQL schema. When migrating from prisma-binding to Prisma Client, the main difference is that the info object can't be used to resolve relations automatically any more, instead you'll need to implement your type resolvers to ensure that relations get resolved properly.

The guide assumes that you already went through the guide for upgrading the Prisma layer. This means you already:

  • installed the Prisma 2 CLI
  • created your Prisma 2 schema
  • introspected your database and resolved potential schema incompatibilities
  • installed and generated Prisma Client

The guide further assumes that you have a file setup that looks similar to this:

1.
2├── README.md
3├── package.json
4├── prisma
5│ └── schema.prisma
6├── prisma1
7│ ├── datamodel.prisma
8│ └── prisma.yml
9└── src
10 ├── generated
11 │ └── prisma.graphql
12 ├── index.js
13 └── schema.graphql

The important parts are:

  • A folder called with prisma with your Prisma 2 schema
  • A folder called src with your application code and a schema called schema.graphql

If this is not what your project structure looks like, you'll need to adjust the instructions in the guide to match your own setup.

1. Adjusting your GraphQL schema

With prisma-binding, your approach for defining your GraphQL schema (sometimes called application schema) is based on importing GraphQL types from the generated prisma.graphql file (in Prisma 1, this is typically called Prisma GraphQL schema). These types mirror the types from your Prisma 1 datamodel and serve as foundation for your GraphQL API.

With Prisma 2, there's no prisma.graphql file any more that you could import from. Therefore, you have to spell out all the types of your GraphQL schema directly inside your schema.graphql file.

The easiest way to do so is by downloading the full GraphQL schema from the GraphQL Playground. To do so, open the SCHEMA tab and click the DOWNLOAD button in the top-right corner, then select SDL:

Alternatively, you can use the get-schema command of the GraphQL CLI to download your full schema:

1npx graphql get-schema --endpoint __GRAPHQL_YOGA_ENDPOINT__ --output schema.graphql --no-all

Note: With the above command, you need to replace the __GRAPHQL_YOGA_ENDPOINT__ placeholder with the actual endpoint of your GraphQL Yoga server.

Once you obtained the schema.graphql file, replace your current version in src/schema.graphql with the new contents. Note that the two schemas are 100% equivalent, except that the new one doesn't use graphql-import for importing types from a different file. Instead, it spells out all types in a single file.

Here's a comparison of these two versions of the sample GraphQL schema that we'll migrate in this guide (you can use the tabs to switch between the two versions):

Before (with grahpql-import)
After (used with Prisma 2)
1# import Post from './generated/prisma.graphql'
2# import User from './generated/prisma.graphql'
3# import Category from './generated/prisma.graphql'
4
5type Query {
6 posts(searchString: String): [Post!]!
7 user(userUniqueInput: UserUniqueInput!): User
8 users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
9 allCategories: [Category!]!
10}
11
12input UserUniqueInput {
13 id: String
14 email: String
15}
16
17type Mutation {
18 createDraft(authorId: ID!, title: String!, content: String!): Post
19 publish(id: ID!): Post
20 deletePost(id: ID!): Post
21 signup(name: String!, email: String!): User!
22 updateBio(userId: String!, bio: String!): User
23 addPostToCategories(postId: String!, categoryIds: [String!]!): Post
24}

You'll notice that the new version of your GraphQL schema not only defines the models that were imported directly, but also additional types (e.g. input types) that were not present in the schema before.

2. Set up your PrismaClient instance

PrismaClient is your new interface to the database in Prisma 2. It lets you invoke various methods which build SQL queries and send them to the database, returning the results as plain JavaScript objects.

The PrismaClient query API is inspired by the initial prisma-binding API, so a lot of the queries you send with Prisma Client will feel familiar.

Similar to the prisma-binding instance from Prisma 1, you also want to attach your PrismaClient from Prisma 2 to GraphQL's context so that in can be accessed inside your resolvers:

1const { PrismaClient } = require('@prisma/client')
2
3// ...
4
5const server = new GraphQLServer({
6 typeDefs: 'src/schema.graphql',
7 resolvers,
8 context: req => ({
9 ...req,
- prisma: new Prisma({
- typeDefs: 'src/generated/prisma.graphql',
- endpoint: 'http://localhost:4466',
- }),
+ prisma: new PrismaClient()
15 }),
16})

In the code block above, the red lines are the lines to be removed from your current setup, the green lines are the ones that you should add. Of course, it's possible that your previous setup differed from this one (e.g. it's unlikely that your Prisma endpoint was http://localhost:4466 if you're running your API in production), this is just a sample to indicate what it could look like.

When you're now accessing context.prisma inside of a resolver, you now have access to the Prisma Client queries.

2. Write your GraphQL type resolvers

prisma-binding was able to magically resolve relations in your GraphQL schema. When not using prisma-binding though, you need to explicitly resolve your relations using so-called type resolvers.

Note You can learn more about the concept of type resolvers and why they're necessary in this article: GraphQL Server Basics: GraphQL Schemas, TypeDefs & Resolvers Explained

2.1. Implementing the type resolver for the User type

The User type in our sample GraphQL schema is defined as follows:

1type User implements Node {
2 id: ID!
3 email: String
4 name: String!
5 posts(
6 where: PostWhereInput
7 orderBy: PostOrderByInput
8 skip: Int
9 after: String
10 before: String
11 first: Int
12 last: Int
13 ): [Post!]
14 role: Role!
15 profile: Profile
16 jsonData: Json
17}

This type has two relations:

  • The posts field denotes a 1-n relation to Post
  • The profile field denotes a 1-1 relation to Profile

Since you're not using prisma-binding any more, you now need to resolve these relations "manually" in type resolvers.

You can do so by adding a User field to your resolver map and implement the resolvers for the posts and profile relations as follows:

1const resolvers = {
2 Query: {
3 // ... your query resolvers
4 },
5 Mutation: {
6 // ... your mutation resolvers
7 },
+ User: {
+ posts: (parent, args, context) => {
+ return context.prisma.user.findOne({
+ where: { id: parent.id }
+ }).posts()
+ },
+ profile: (parent, args, context) => {
+ return context.prisma.user.findOne({
+ where: { id: parent.id }
+ }).profile()
+ }
+ },
20}

Inside of these resolvers, you're using your new PrismaClient to perform a query against the database. Inside the posts resolver, the database query loads all Post records from the specified author (whose id is carried in the parent object). Inside the profile resolver, the database query loads the Profile record from the specified user (whose id is carried in the parent object).

Thanks to these extra resolvers, you'll now be able to nest relations in your GraphQL queries/mutations whenever you're requesting information about the User type in a query, e.g.:

1{
2 users {
3 id
4 name
5 posts { # fetching this relation is enabled by the new type resolver
6 id
7 title
8 }
9 profile { # fetching this relation is enabled by the new type resolver
10 id
11 bio
12 }
13 }
14}

2.2. Implementing the type resolver for the Post type

The Post type in our sample GraphQL schema is defined as follows:

1type Post implements Node {
2 id: ID!
3 createdAt: DateTime!
4 updatedAt: DateTime!
5 title: String!
6 content: String
7 published: Boolean!
8 author: User
9 categories(
10 where: CategoryWhereInput
11 orderBy: CategoryOrderByInput
12 skip: Int
13 after: String
14 before: String
15 first: Int
16 last: Int
17 ): [Category!]
18}

This type has two relations:

  • The author field denotes a 1-n relation to User
  • The categories field denotes a m-n relation to Category

Since you're not using prisma-binding any more, you now need to resolve these relations "manually" in type resolvers.

You can do so by adding a Post field to your resolver map and implement the resolvers for the author and categories relations as follows:

1const resolvers = {
2 Query: {
3 // ... your query resolvers
4 },
5 Mutation: {
6 // ... your mutation resolvers
7 },
8 User: {
9 // ... your type resolvers for `User` from before
10 },
+ Post: {
+ author: (parent, args, context) => {
+ return context.prisma.post.findOne({
+ where: { id: parent.id }
+ }).author()
+ },
+ categories: (parent, args, context) => {
+ return context.prisma.post.findOne({
+ where: { id: parent.id }
+ }).categories()
+ }
+ },
23}

Inside of these resolvers, you're using your new PrismaClient to perform a query against the database. Inside the author resolver, the database query loads the User record that represents the author of the Post. Inside the categories resolver, the database query loads all Category records from the specified post (whose id is carried in the parent object).

Thanks to these extra resolvers, you'll now be able to nest relations in your GraphQL queries/mutations whenever you're requesting information about the User type in a query, e.g.:

1{
2 posts {
3 id
4 title
5 author { # fetching this relation is enabled by the new type resolver
6 id
7 name
8 }
9 categories { # fetching this relation is enabled by the new type resolver
10 id
11 name
12 }
13 }
14}

2.3. Implementing the type resolver for the Profile type

The Profile type in our sample GraphQL schema is defined as follows:

1type Profile implements Node {
2 id: ID!
3 bio: String
4 user: User!
5}

This type has one relation: The user field denotes a 1-n relation to User.

Since you're not using prisma-binding any more, you now need to resolve this relation "manually" in type resolvers.

You can do so by adding a Profile field to your resolver map and implement the resolvers for the owner relation as follows:

1const resolvers = {
2 Query: {
3 // ... your query resolvers
4 },
5 Mutation: {
6 // ... your mutation resolvers
7 },
8 User: {
9 // ... your type resolvers for `User` from before
10 },
11 Post: {
12 // ... your type resolvers for `Post` from before
13 },
+ Profile: {
+ user: (parent, args, context) => {
+ return context.prisma.profile.findOne({
+ where: { id: parent.id }
+ }).owner()
+ }
+ },
21}

Inside of this resolver, you're using your new PrismaClient to perform a query against the database. Inside the user resolver, the database query loads the User records from the specified profile (whose id is carried in the parent object).

Thanks to this extra resolver, you'll now be able to nest relations in your GraphQL queries/mutations whenever you're requesting information about the Profile type in a query.

2.4. Implementing the type resolver for the Category type

The Category type in our sample GraphQL schema is defined as follows:

1type Category implements Node {
2 id: ID!
3 name: String!
4 posts(
5 where: PostWhereInput
6 orderBy: PostOrderByInput
7 skip: Int
8 after: String
9 before: String
10 first: Int
11 last: Int
12 ): [Post!]
13}

This type has one relation: The posts field denotes a m-n relation to Post.

Since you're not using prisma-binding any more, you now need to resolve this relation "manually" in type resolvers.

You can do so by adding a Category field to your resolver map and implement the resolvers for the posts and profile relations as follows:

1const resolvers = {
2 Query: {
3 // ... your query resolvers
4 },
5 Mutation: {
6 // ... your mutation resolvers
7 },
8 User: {
9 // ... your type resolvers for `User` from before
10 },
11 Post: {
12 // ... your type resolvers for `Post` from before
13 },
14 Profile: {
15 // ... your type resolvers for `User` from before
16 },
+ Category: {
+ posts: (parent, args, context) => {
+ return context.prisma.findOne({
+ where: { id: parent.id }
+ }).posts()
+ }
+ },
24}

Inside of this resolver, you're using your new PrismaClient to perform a query against the database. Inside the posts resolver, the database query loads all Post records from the specified categories (whose id is carried in the parent object).

Thanks to this extra resolver, you'll now be able to nest relations in your GraphQL queries/mutations whenever you're requesting information about a Category type in a query.

With all your type resolvers in place, you can start migrating the actual GraphQL API operations.

3. Migrate GraphQL operations

3.1. Migrate GraphQL queries

In this section, you'll migrate all GraphQL queries from prisma-binding to Prisma Client.

3.1.1. Migrate the users query (which uses forwardTo)

In our sample API, the users query from the sample GraphQL schema is defined and implemented as follows.

SDL schema definition with prisma-binding
1type Query {
2 users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
3 # ... other queries
4}
Resolver implementation with prisma-binding
1const resolvers = {
2 Query: {
3 users: forwardTo('prisma'),
4 // ... other resolvers
5 }
6}
Implementing the users resolver with Prisma Client

To re-implement queries that were previously using forwardTo, the idea is to pass the incoming filtering, ordering and pagination arguments to PrismaClient:

1const resolvers = {
2 Query: {
3 users: (_, args, context, info) => {
4 // this doesn't work yet
5 const { where, orderBy, skip, first, last, after, before } = args
6 return context.prismaClient.user.findMany({
7 where,
8 orderBy,
9 skip,
10 first,
11 last,
12 after,
13 before
14 })
15 },
16 // ... other resolvers
17 }
18}

Note that this approach does not work yet because the structures of the incoming arguments is different from the ones expected by PrismaClient. To ensure the structures are compatible, you can use the prisma-binding-argument-transform npm package which ensures compatibility:

1npm install @prisma/binding-argument-transform

You can now use this package as follows:

1const { makeOrderByPrisma2Compatible, makeWherePrisma2Compatible } = require('@prisma/binding-argument-transform')
2
3const resolvers = {
4 Query: {
5 users: (_, args, context, info) => {
6 // this still doesn't entirely work
7 const { where, orderBy, skip, first, last, after, before } = args
8 const prisma2Where = makeWherePrisma2Compatible(where)
9 const prisma2OrderBy = makeOrderByPrisma2Compatible(orderBy)
10 return context.prisma.user.findMany({
11 where: prisma2Where,
12 orderBy: prisma2OrderBy,
13 skip,
14 first,
15 last,
16 after,before
17 })
18 },
19 // ... other resolvers
20 }
21}

The last remaining issue with this are the pagination arguments. Prisma 2 introduces a new pagination API:

  • The first, last, before and after arguments are removed
  • The new cursor argument replaces before and after
  • The new take argument replaces first and last

Here is how you can adjust the call to make it compliant with the new Prisma Client pagination API:

1const { makeOrderByPrisma2Compatible, makeWherePrisma2Compatible } = require('prisma-binding-argument-transform')
2
3const resolvers = {
4 Query: {
5 users: (_, args, context) => {
6 const { where, orderBy, skip, first, last, after, before } = args
7 const prisma2Where = makeWherePrisma2Compatible(where)
8 const prisma2OrderBy = makeOrderByPrisma2Compatible(orderBy)
9 const skipValue = skip || 0
10 const prisma2Skip = Boolean(before) ? skipValue+1 : skipValue
11 const prisma2Take = Boolean(last) ? -last : first
12 const prisma2Before = { id: before }
13 const prisma2After = { id: after }
14 const prisma2Cursor = !Boolean(before) && !Boolean(after) ? undefined : (Boolean(before) ? prisma2Before : prisma2After)
15 return context.prismaClient.user.findMany({
16 where: prisma2Where,
17 orderBy: prisma2OrderBy,
18 skip: prisma2Skip,
19 cursor: prisma2Cursor,
20 take: prisma2Take,
21 })
22 },
23 // ... other resolvers
24 }
25}

The calculations are needed to ensure the incoming pagination arguments map properly to the ones from the Prisma Client API.

3.1.2. Migrate the posts(searchString: String): [Post!]! query

The posts query is defined and implemented as follows.

SDL schema definition with prisma-binding
1type Query {
2 posts(searchString: String): [Post!]!
3 # ... other queries
4}
Resolver implementation with prisma-binding
1const resolvers = {
2 Query: {
3 posts: (_, args, context, info) => {
4 return context.prisma.query.posts(
5 {
6 where: {
7 OR: [
8 { title_contains: args.searchString },
9 { content_contains: args.searchString },
10 ],
11 },
12 },
13 info,
14 )
15 },
16 // ... other resolvers
17 }
18}
Implementing the posts resolver with Prisma Client

To get the same behaviour with the new Prisma Client, you'll need to adjust your resolver implementation:

1const resolvers = {
2 Query: {
3 posts: (_, args, context) => {
4 return context.prisma.post.findMany({
5 where: {
6 OR: [
7 { title: { contains: args.searchString } },
8 { content: { contains: args.searchString } },
9 ],
10 },
11 })
12 },
13 // ... other resolvers
14 }
15}

You can now send the respective query in the GraphQL Playground:

1{
2 posts {
3 id
4 title
5 author {
6 id
7 name
8 }
9 }
10}

3.1.3. Migrate the user(uniqueInput: UserUniqueInput): User query

In our sample app, the user query is defined and implemented as follows.

SDL schema definition with prisma-binding
1type Query {
2 user(userUniqueInput: UserUniqueInput): User
3 # ... other queries
4}
5
6input UserUniqueInput {
7 id: String
8 email: String
9}
Resolver implementation with prisma-binding
1const resolvers = {
2 Query: {
3 user: (_, args, context, info) => {
4 return context.prisma.query.user(
5 {
6 where: args.userUniqueInput,
7 },
8 info,
9 )
10 },
11 // ... other resolvers
12 }
13}
Implementing the user resolver with Prisma Client

To get the same behaviour with the new Prisma Client, you'll need to adjust your resolver implementation:

1const resolvers = {
2 Query: {
3 user: (_, args, context) => {
4 return context.prisma.user.findOne({
5 where: args.userUniqueInput,
6 })
7 }
8 // ... other resolvers
9 }
10}

You can now send the respective query via the GraphQL Playground:

1{
2 user(userUniqueInput: { email: "alice@prisma.io" }) {
3 id
4 name
5 }
6}

3.1. Migrate GraphQL mutations

In this section, you'll migrate the GraphQL mutations from the sample schema.

3.1.2. Migrate the createUser mutation (which uses forwardTo)

In the sample app, the createUser mutation from the sample GraphQL schema is defined and implemented as follows.

SDL schema definition with prisma-binding
1type Mutation {
2 createUser(data: UserCreateInput!): User!
3 # ... other mutations
4}
Resolver implementation with prisma-binding
1const resolvers = {
2 Mutation: {
3 createUser: forwardTo('prisma'),
4 // ... other resolvers
5 }
6}
Implementing the createUser resolver with Prisma Client

To get the same behaviour with the new Prisma Client, you'll need to adjust your resolver implementation:

1const resolvers = {
2 Mutation: {
3 createUser: (_, args, context, info) => {
4 return context.prisma.user.create({
5 data: args.data
6 })
7 },
8 // ... other resolvers
9 }
10}

You can now write your first mutation against the new API, e.g.:

1mutation {
2 createUser(data: {
3 name: "Alice",
4 email: "alice@prisma.io"
5 }) {
6 id
7 }
8}

3.1.3. Migrate the createDraft(title: String!, content: String, authorId: String!): Post! query

In the sample app, the createDraft mutation is defined and implemented as follows.

SDL schema definition with prisma-binding
1type Mutation {
2 createDraft(title: String!, content: String, authorId: String!): Post!
3 # ... other mutations
4}
Resolver implementation with prisma-binding
1const resolvers = {
2 Mutation: {
3 createDraft: (_, args, context, info) => {
4 return context.prisma.mutation.createPost(
5 {
6 data: {
7 title: args.title,
8 content: args.content,
9 author: {
10 connect: {
11 id: args.authorId,
12 },
13 },
14 },
15 },
16 info,
17 )
18 },
19 // ... other resolvers
20 }
21}
Implementing the createDraft resolver with Prisma Client

To get the same behaviour with the new Prisma Client, you'll need to adjust your resolver implementation:

1const resolvers = {
2 Mutation: {
3 createDraft: (_, args, context, info) => {
4 return context.prisma.post.create({
5 data: {
6 title: args.title,
7 content: args.content,
8 author: {
9 connect: {
10 id: args.authorId,
11 },
12 },
13 },
14 })
15 }
16 // ... other resolvers
17 }
18}

You can now send the respective mutation via the GraphQL Playground:

1mutation {
2 createDraft(
3 title: "Hello World",
4 authorId: "__AUTHOR_ID__"
5 ) {
6 id
7 published
8 author {
9 id
10 name
11 }
12 }
13}

3.1.4. Migrate the updateBio(bio: String, userUniqueInput: UserUniqueInput!): User mutation

In the sample app, the updateBio mutation is defined and implemented as follows.

SDL schema definition with prisma-binding
1type Mutation {
2 updateBio(bio: String!, userUniqueInput: UserUniqueInput!): User
3 # ... other mutations
4}
Resolver implementation with prisma-binding
1const resolvers = {
2 Mutation: {
3 updateBio: (_, args, context, info) => {
4 return context.prisma.mutation.updateUser(
5 {
6 data: {
7 profile: {
8 update: { bio: args.bio }
9 }
10 },
11 where: { id: args.userId }
12 },
13 info,
14 )
15 },
16 // ... other resolvers
17 }
18}
Implementing the updateBio resolver with Prisma Client

To get the same behaviour with Prisma Client, you'll need to adjust your resolver implementation:

1const resolvers = {
2 Mutation: {
3 updateBio: (_, args, context, info) => {
4 return context.prisma.user.update({
5 data: {
6 profile: {
7 update: { bio: args.bio }
8 }
9 },
10 where: args.userUniqueInput
11 })
12 },
13 // ... other resolvers
14 }
15}

You can now send the respective mutation via the GraphQL Playground :

1mutation {
2 updateBio(
3 userUniqueInput: {
4 email: "alice@prisma.io"
5 }
6 bio: "I like turtles"
7 ){
8 id
9 name
10 profile {
11 id
12 bio
13 }
14 }
15}

3.1.5. Migrate the addPostToCategories(postId: String!, categoryIds: [String!]!): Post mutation

In our sample app, the addPostToCategories mutation is defined and implemented as follows.

SDL schema definition with prisma-binding
1type Mutation {
2 addPostToCategories(postId: String!, categoryIds: [String!]!): Post
3 # ... other mutations
4}
Resolver implementation with prisma-binding
1const resolvers = {
2 Mutation: {
3 addPostToCategories: (_, args, context, info) => {
4 const ids = args.categoryIds.map(id => ({ id }))
5 return context.prisma.mutation.updatePost(
6 {
7 data: {
8 categories: {
9 connect: ids
10 }
11 },
12 where: {
13 id: args.postId
14 }
15 },
16 info,
17 )
18 },
19 // ... other resolvers
20 }
21}
Implementing the addPostToCategories resolver with Prisma Client

To get the same behaviour with Prisma Client, you'll need to adjust your resolver implementation:

1const resolvers = {
2 Mutation: {
3 addPostToCategories: (_, args, context, info) => {
4 const ids = args.categoryIds.map(id => ({ id }))
5 return context.prisma.post.update({
6 where: {
7 id: args.postId
8 },
9 data: {
10 categories: { connect: ids }
11 }
12 })
13 },
14 // ... other resolvers
15 }
16}

You can now send the respective query via the GraphQL Playground:

1mutation {
2 addPostToCategories(
3 postId: "__AUTHOR_ID__"
4 categoryIds: [
5 "__CATEGORY_ID_1__",
6 "__CATEGORY_ID_2__",
7 ]
8 ) {
9 id
10 title
11 categories {
12 id
13 name
14 }
15 }
16}

4. Cleaning up

Since the entire app has now been upgraded to Prisma 2, you can delete all unnecessary files and remove the no longer needed dependencies.

4.1. Clean up npm dependencies

You can start by removing npm dependencies that were related to the Prisma 1 setup:

1npm uninstall graphql-cli graphql-yoga prisma-binding prisma1

4.2. Delete unused files

Next, delete the files of your Prisma 1 setup:

1rm prisma1/datamodel.prisma prisma1/prisma.yml

You can also delete any remaining .js files, the schema.graphql and prisma.graphql files.

4.3. Stop the Prisma server

Finally, you can stop running your Prisma server.

Edit this page on Github