Part 2: Building a GraphQL API with Prisma

Table of contents

Introduction

In this article, you’ll learn how to integrate Prisma and MongoDB into your GraphQL API.

This article is the last section of a 2 part series, you can check out part 1 here.

Debrief on Part 1 of the series:

  1. Cloned the frontend Next.js template.
  2. Set up the project with GraphQL, GraphQL request, Prisma, GraphQL Yoga, and MongoDB.
  3. Built GraphQL queries and mutations using dummy data from JSON files.
  4. Changed the frontend to fetch data from our GraphQL API.

You can pick up where Part 1 leaves off:

git clone --branch part-1-complete https://github.com/sonylomo/Graphql-ToDo-List.git
yarn install

Prisma Setup

0. MongoDB initialization

Create a new project in MongoDB. You’ll need its database URL for Prisma.

If you’re not familiar with the process, you can follow this guide: https://www.mongodb.com/basics/create-database

1. Installation

Install Prisma as a dev dependency:

yarn add prisma -D

When the installation is done, .env and prisma/schema.prisma files will be created. Otherwise, you should add them manually.

In the .env file, add your MongoDB database URL to a DATABASE_URL variable.

DATABASE_URL="mongodb+srv://to-do-admin:flh2ot2r5SftzLDN@cluster0.xwcw1.mongodb.net/to-do?retryWrites=true&w=majority"

In the prisma folder, the schema.prisma file should have the code below:

generator client 
  provider = "prisma-client-js"
}

datasource db {
  provider = "mongodb"
  url      = env("DATABASE_URL")
}

Ensure the database provider is “mongodb”.

2. Define your database models

Your application will have 2 collections in your MongoDB database: Task and Tag.

Update the schema.prisma file with the necessary models:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mongodb"
  url      = env("DATABASE_URL")
}

model Task {
  id          String  @id @default(auto()) @map("_id") @db.ObjectId
  description String
  complete    Boolean
  tag         Tag     @relation(fields: [tagId], references: [id])
  tagId       String  @db.ObjectId
}

model Tag {
  id    String @id @default(auto()) @map("_id") @db.ObjectId
  name  String @unique
  tasks Task[]
}

3. Generate Prisma Client

For us to interact programmatically with your database, you need to install the Prisma Client.

yarn add @prisma/client

The install automatically invokes prisma generate that reads your Prisma schema and generates a Prisma Client custom made for your database models.

Remember to manually run prisma generate when you make any future changes to your Prisma schema.

Prisma Client also generates custom types from your schema.prisma file. You can replace the types imported from utils/types.ts file with types from Prisma Client.

import type { Task, Tag } from "@prisma/client";

Click here to read further on Prisma setup with MongoDB.

Tweaking the GraphQL Resolvers

From Part 1 article of this series, your graphql.ts file probably looks like this:

```tsx
import { createServer } from '@graphql-yoga/node'
import { NextApiRequest, NextApiResponse } from 'next'
import tasks_data from "../../utils/tasks.json"
import tags_data from "../../utils/tags.json"
import { tagProps, taskProps } from '../../utils/types'

const server = createServer<{
  req: NextApiRequest
  res: NextApiResponse
}>({
  schema: {
    typeDefs: /* GraphQL */ `
		 type Tag{
        id:    String! 
        name:  String!
        tasks: [Task]
      }

      type Task {
        id:         String!
        description: String!
        complete:    Boolean!
        tag: Tag
      }

      type Query {
        getAllTasks:[Task!]!
        getAllTags :[Tag!]!
        getTaskByID(id:String!) : Task
      }

      type Mutation {
        addTask(description:String!, tagName :String!):Task!
        updateTask(id:String!, description:String, complete: Boolean, tagName: String!):Task!
        deleteTask(id:String!):Task!
      }
      `,

    resolvers: {
      Tag:
      {
        id: (parent: tagProps) => parent.id,
        name: (parent: tagProps) => parent.name,
        tasks: (parent: any) => {
          return parent.tasks.map(({ id, description, complete }: taskProps) => ({
            id, description, complete
          }))
        }
      },

      Task:
      {
        id: (parent: taskProps) => parent.id,
        description: (parent: taskProps) => parent.description,
        complete: (parent: taskProps) => parent.complete,
        tag: (parent: any) => ({
          id: parent.tag.id,
          name: parent.tag.name,
        })
      },

      Query: {
        getAllTasks: () => tasks_data,
        getAllTags: () => tags_data,
        getTaskByID: (id: string) => {
          return tasks_data.filter((task) => { return (task['id'] == id); })
        }
      },

      Mutation: {
        addTask: (parent: unknown, args: { description: string; tagName: string }) => {
          const newTask = {
            id: "5",
            description: args.description,
            complete: false,
            tag: {
              id: "10",
              name: args.tagName
            }
          };
          tasks_data.push(newTask);
          return newTask;
        },

        updateTask: (parent: unknown, args: { id: string, description: string, complete: boolean, tagName: string }) => {
          const updateTask = tasks_data.find(i => i.id === args.id)

          if (updateTask) {
            updateTask.description = args.description
            updateTask.complete = args.complete
            updateTask.tag.name = args.tagName

            return updateTask
          }
          throw new Error('Id not found');
        },

        deleteTask: (parent: unknown, args: { id: string }) => {
          const idx = tasks_data.findIndex(i => i.id === args.id)

          if (idx !== -1) {
            tasks_data.splice(idx, 1)
            return args.id
          }

          throw new Error('Id not found');
        }
      }
    }
  }
})

export default server
```

Since you are integrating Prisma into your GraphQL API, you’ll use CRUD operations (Create, Read, Update and Delete) with your Prisma Client API. All your Prisma Client queries will be written in the resolvers.

Import PrismaClient and instantiate it in your graphql.ts file.

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

1. Queries

getAllTasks

This query uses [findMany](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#findmany) to get all tasks. You should add [include](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#include) because you’re making query with a related record - Tag.

resolvers: {
	//(...other resolvers)

	Query: {
	  getAllTasks: async () => {
	    return await prisma.task.findMany({
	      include: {
	        tag: true
	      }
	    })
	  },
	}
}

getAllTags

This query is slightly similar to the previous one.

resolvers: {
	//(...other resolvers)

	Query: {
	  getAllTasks: (...)

		getAllTags: async () => {
      return await prisma.tag.findMany({
        include: {
          tasks: true
        }
      })
    },
	}
}

getTaskByID

This query retrieves one task by it’s id using [findUnique](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#findunique)

resolvers: {
	//(...other resolvers)

	Query: {
	  getAllTasks: (...)
		getAllTags: (...)

		getTaskByID: async (_, args: { id: string }) => {
      return await prisma.task.findUnique({
        where: {
          id: args.id
        },
        include: {
          tag: true
        }
      })
    },
  },

2. Mutations

addTask

This mutation creates a new task with the specified description and tag name. It utilizes [connectOrCreate](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#connectorcreate) query to check if a tag name exists. If it’s unavailable in the database, it’ll create a new tag record.

resolvers: {
	//(...other resolvers)

	Mutation: {
	 addTask: async (parent: unknown, args: { description: string; tagName: string }) => {
	    const newTask = await prisma.task.create({
	      data: {
	        description: args.description,
	        complete: false,
	        tag: {
	          connectOrCreate: {
	            where: {
	              name: args.tagName
	            }, create: {
	              name: args.tagName
	            }
	          }
	        }
	      }, include: {
	        tag: true
	      }
	    })
	    return newTask;
	  },
}

updateTask

This mutation updates the task that matches the passed id. It can change the task description, completion, or tag name.

resolvers: {
	//(...other resolvers)

	Mutation: {
	  addTask: (...)

		updateTask: async (parent: unknown, args: { id: string, description: string, complete: boolean, tagName: string }) => {
	    const updateTask = await prisma.task.update({
        where: {
          id: args.id
        },
        data: {
          description: args.description,
          complete: args.complete,
          tag: {
            connectOrCreate: {
              where: {
                name: args.tagName
              }, create: {
                name: args.tagName
              }
            }
          }
        },
        include: {
          tag: true
        }
      })
      return updateTask;
	  },
}

deleteTask

This mutation deletes the task that matches the argument id.

resolvers: {
	//(...other resolvers)

	Mutation: {
	  addTask: (...),

		updateTask: (...),

		deleteTask: async (parent: unknown, args: { id: string }) => {
      const deleteTask = await prisma.task.delete({
        where: {
          id: args.id,
        },
        include: {
          tag: true
        }
      })
      return deleteTask;
    },
}

At this point, your graphql.ts file should look something close to this.

import { createServer } from '@graphql-yoga/node'
import { PrismaClient } from '@prisma/client'
import { NextApiRequest, NextApiResponse } from 'next'

const prisma = new PrismaClient()

const server = createServer<{
  req: NextApiRequest
  res: NextApiResponse
}>({
  schema: {
    typeDefs: /* GraphQL */ `
      type Task {
        id:         String!
        description: String!
        complete:    Boolean!
        tag: Tag
      }

      type Tag{
        id:    String! 
        name:  String!
        tasks: [Task]
      }
      
      type Query {
        getAllTasks:[Task!]!
        getAllTags :[Tag!]!
        getTaskByID(id:String!) : Task
      }

      type Mutation {
        addTask(description:String!, tagName :String!):Task!
        updateTask(id:String!, description:String, complete: Boolean, tagName: String!):Task!
        deleteTask(id:String!):Task!
      }
      `,

    resolvers: {
      Tag:
      {
        id: (parent: Tag) => parent.id,
        name: (parent: Tag) => parent.name,
        //fix type
        tasks: (parent: any) => {
          return parent.tasks.map(({ id, description, complete }: Task) => ({
            id, description, complete
          }))
        }
      },

      Task:
      {
        id: (parent: Task) => parent.id,
        description: (parent: Task) => parent.description,
        complete: (parent: Task) => parent.complete,
        tag: (parent: any) => ({
          id: parent.tag.id,
          name: parent.tag.name,
        })
      },

      Query: {
        getAllTasks: async () => {
          return await prisma.task.findMany({
            include: {
              tag: true
            }
          })
        },

        getAllTags: async () => {
          return await prisma.tag.findMany({
            include: {
              tasks: true
            }
          })
        },

        getTaskByID: async (_, args: { id: string }) => {
          return await prisma.task.findUnique({
            where: {
              id: args.id
            },
            include: {
              tag: true
            }
          })
        },
      },

      Mutation: {
        addTask: async (parent: unknown, args: { description: string; tagName: string }) => {
          const newTask = await prisma.task.create({
            data: {
              description: args.description,
              complete: false,
              tag: {
                connectOrCreate: {
                  where: {
                    name: args.tagName
                  }, create: {
                    name: args.tagName
                  }
                }
              }
            }, include: {
              tag: true
            }
          })
          return newTask;
        },

        updateTask: async (parent: unknown, args: { id: string, description: string, complete: boolean, tagName: string }) => {
          const updateTask = await prisma.task.update({
            where: {
              id: args.id
            },
            data: {
              description: args.description,
              complete: args.complete,
              tag: {
                connectOrCreate: {
                  where: {
                    name: args.tagName
                  }, create: {
                    name: args.tagName
                  }
                }
              }
            },
            include: {
              tag: true
            }
          })
          return updateTask;
        },

        deleteTask: async (parent: unknown, args: { id: string }) => {
          const deleteTask = await prisma.task.delete({
            where: {
              id: args.id,
            },
            include: {
              tag: true
            }
          })
          return deleteTask;
        },
      }
    }
  }
})

export default server

Testing your GraphQL API

1. Prisma Studio

One of the perks of using Prisma is being able to leverage Prisma Studio - a visual editor for the data in your database. But first things first, you’ll have to push your Prisma schema state to the database.

Run this command on your terminal.

prisma db push

If you view your MongoDB project, there should see 2 collections - Tag and Task.

To be able to manipulate your data from Prisma Studio, run this command in your terminal:

npx prisma studio

You will be redirected to http://localhost:5555/ in your browser. You can manually add records into your database from Prisma Studio. Saving the changes you make will sync your data to MongoDB.

prisma_studio

Alternatives to using Prisma Studio:

2. Yoga GraphiQL Playground

get-all-tags

You can also add query variables for any mutations or queries that require arguments:

testing-with-variables

3. Your Next.js Frontend

In Part 1 of this series, you connected the frontend to the server already. It should work fine at this point.

complete-todo-app

You can find the complete To-Do List code on GitHub.

Wrapping Up 🎉

Pat yourself on the back for reaching this far. Feel free to add new features to the frontend and make it your own. You’re just getting started.

https://media.giphy.com/media/IVEIuCwXmZnQhMll03/giphy.gif

Also, check out these resources that really helped me get it:

Sonia Lomo

© 2024 Sonia Lomo

LinkedIn đť•Ź GitHub