- Published on
Part 2: Building a GraphQL API with Prisma
- Authors
- Name
- Sonia Lomo
- @sony_lomo
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:
- Cloned the frontend Next.js template.
- Set up the project with GraphQL, GraphQL request, Prisma, GraphQL Yoga, and MongoDB.
- Built GraphQL queries and mutations using dummy data from JSON files.
- 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:
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.
Alternatives to using Prisma Studio:
- adding records directly from your MongoDB database
- creating a script to seed data into your database
2. Yoga GraphiQL Playground
You can also add query variables for any mutations or queries that require arguments:
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.
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.
Also, check out these resources that really helped me get it: