This article is the first half of a 2 part series. In the series, you’ll learn how to build a GraphQL API with GraphQL Yoga, connect it to MongoDB and glue it together with Prisma, a next-generation ORM. You’ll also attach your GraphQL API to an existing Next.js frontend.
Table of contents
- Introduction
- Project Setup
- Building our GraphQL API
- A simple query
- Adding All Queries
- Adding All Mutations
- In Conclusion
Introduction
Traditionally, REST APIs have been used and it has proven to have some limitations like over fetching data and multiple network requests, that are being remedied by GraphQL APIs. GraphQL is a query language for reading and mutating data in APIs. It describes a type system of schema for your data in the backend API. This allows frontend consumers to query the exact data that they need.
In this tutorial, you’ll only build a GraphQL API and connect it to your To-Do List App.
Prerequisites
- Basic knowledge of React
- Enthusiasm to learn
Technologies you will use:
- Frontend
- TypeScript - a strongly typed programming language that builds on JavaScript.
- Next.js (with TypeScript config) - JavaScript framework that lets you build server-side-rendered and static web apps using React.
- GraphQL request - a simple GraphQL client that helps you send queries and mutations with a single line of code.
- Backend
- GraphQL Yoga - GraphQL server that’s focused on easy setup for building your API. If you’re familiar with building a REST API with Node.js, GraphQL Yoga is similar to using the Express.js framework for your server.
- Prisma - the bridge between your MongoDB database and your code. You’ll use it inside your GraphQL resolvers in a later section.
- MongoDB - a cross-platform document-oriented database program for your app.
Project Setup
I created a Next.js starter template for this project for you to clone if you’d like to build the full-stack app.
Run this command on your terminal:
git clone --branch starter-template https://github.com/sonylomo/Graphql-ToDo-List.git
yarn install
Install all the packages you’ll need for your API:
yarn add graphql graphql-request @graphql-yoga/node
Run yarn dev
to open your app on http://localhost:3000/
Building our GraphQL API
Some jargon you should know before you get started.
query
- used to read or fetch data.mutation
- used to write or post data.typeDefinitions
- your GraphQL schema definition.resolvers
- the resolver functions are part of the GraphQL schema, and they are the actual implementation (code/logic) of the GraphQL schema definitions.schema
- a combination of the GraphQL SDL and the resolvers.
GraphQL server
Inside your api/graph.ts
file, add the following code:
// 1. Import GraphQL Yoga
import { createServer } from '@graphql-yoga/node'
// 2. Create your server
const server = createServer({
schema: {
typeDefs: /* GraphQL */ `
type Query {
hello: String!
}
`,
resolvers: {
Query: {
hello: () => "What's cookin', good lookin'?"
}
}
}
})
// 3. Serve the API and GraphiQL
export default server
GraphQL Yoga comes with an integrated Yoga GraphiQL playground. You can open it at http://localhost:3000/api/graphql.
The playground has an explorer on the top left corner to help you add existing queries with ease. On the top-right corner, you’ll see the self-documented queries that you’ll add later on. GraphQL takes your type definitions and creates beautiful documentation for your API.
A simple query
In the Yoga GraphiQL playground, add the following query:
query {
hello
}
On the frontend, you can add this to your index.tsx
file. It’s a rough implementation of how you’ll dynamically make your queries and mutations on the frontend.
// 1. import gql and request
import { gql, request } from "graphql-request";
//(...other imports)
//2. write your first query
const myQuery = gql`
query {
hello
}
`;
const Home: NextPage = () => {
useEffect(() => {
//3. pass your query to your server at api/graphql
request("/api/graphql", myQuery).then((res) => {
// 4. log out the response
console.log("first query", res);
});
}, []);
return (...)
}
You should see your response on the console with your response.
Now that you have the basics laid down, let’s kick it up a notch!
Adding All Queries
You’re going to define queries needed for your ToDo app to your server. They’ll retrieve dummy data from tasks.json
and tags.json
files in the utils
folder.
Add these imports to your graphql.ts
file:
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'
You will create 3 queries: getAllTasks
, getAllTags
andgetTaskByID
.
getAllTasks
Replace the type Query you made with:
typeDefs: /* GraphQL */ `
type Tag{
id: String!
name: String!
tasks: [Task]
}
type Task {
id: String!
description: String!
complete: Boolean!
tag: Tag
}
type Query {
getAllTasks:[Task!]!
}
`
You’re creating a query to get all the tasks from your database (or in your case, the tasks.json
file). Add a corresponding resolver for each type you’ve added.
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: {
//tasks_data is imported dummy data from tasks.json
getAllTasks: () => tasks_data,
}
}
Before you continue, there’s one important thing you need to note. Resolver functions typically take in 4 input arguments: parent
, args
, context
and info
.
root
(also sometimes calledparent
): All that a GraphQL server needs to do to resolve a query is call the resolvers of the query’s fields. It’s doing so breadth-first (level-by-level) and theroot
argument in each resolver call is simply the result of the previous call (initial value isnull
if not otherwise specified).
args
: This argument carries the parameters for the query, for example, theid
ordescription
of aTask
to be fetched.
context
: An object that gets passed through the resolver chain that each resolver can write to and read from (basically a means for resolvers to communicate and share information).
info
: An AST representation of the query or mutation. You can read more about the details in part III of this series: Demystifying the info Argument in GraphQL Resolvers.
You can read more about resolvers here.
Add this query to the index.tsx
file and make a request to your server. It’ll get all tasks to be displayed on the app.
// 1. import graphql-request methods
import { gql}, request from "graphql-request";
// 2. declare a variable with your query
const AllTasksTags = gql`
query AllTasksTags {
getAllTasks {
id
description
complete
tag {
id
name
}
}
}
`;
const Home: NextPage = () => {
const [tasks, setTasks] = useState([]);
const [allTags, setAllTags] = useState([]);
const [newTag, setNewTag] = useState("");
const [newDescription, setNewDescription] = useState("");
useEffect(() => {
// 3. make a request for data from server
request("/api/graphql", AllTasksTags).then((res) => {
// 4. update state with response containing all tasks
setTasks(res.getAllTasks);
console.log("first query", res.getAllTasks);
});
}, []);
return (...)
}
You should see a list of Tasks on your console. Or better yet, try out the Yoga GraphiQL playground.
getAllTags
The next query gets all tags available in the database:
typeDefs: /* GraphQL */ `
(...other types)
type Query {
getAllTasks:[Task!]!
getAllTags :[Tag!]!
}
`
It’s corresponding resolver:
resolvers: {
//(...other resolvers)
Query: {
getAllTasks: () => tasks_data,
//tags_data is imported dummy data from tags.json
getAllTags: () => tags_data
}
}
On the frontend, add the following to index.tsx
. It’ll get all tags to be displayed on the app.
// 1. nest your query to get both tasks and tags
const AllTasksTags = gql`
query AllTasksTags {
getAllTags {
id
name
}
getAllTasks {
id
description
complete
tag {
id
name
}
}
}
`;
const Home: NextPage = () => {
const [tasks, setTasks] = useState([]);
const [allTags, setAllTags] = useState([]);
const [newTag, setNewTag] = useState("");
const [newDescription, setNewDescription] = useState("");
useEffect(() => {
// 2. make a request for data from server
request("/api/graphql", AllTasksTags).then((res) => {
// 3. update state with response containing all tasks
setTasks(res.getAllTasks);
//4. update state with response containing all tags
setAllTags(res.getAllTags);
console.log("first query", res.getAllTags);
});
}, []);
return (...)
You should see a list of Tags on your console. Or you could test it out on the Yoga GraphiQL playground.
getTaskByID
Lastly, add a query to retrieve one task with the specified task ID from the database.
typeDefs: /* GraphQL */ `
(...other types)
type Query {
getAllTasks:[Task!]!
getAllTags :[Tag!]!
getTaskByID(id:String!) : Task
}
`
The id
is passed as a required filter for the query to work.
It’s corresponding resolver:
resolvers: {
//(...other resolvers)
Query: {
getAllTasks: () => tasks_data,
getAllTags: () => tags_data,
//returns object with matching id
getTaskByID: (parent: unknown, args: { id: string }) => {
return tasks_data.filter((task) => { return (task['id'] == args.id); })
}
}
}
When a user clicks the task edit button, they are redirected to a new page with that task’s description and tag name. This page is rendered by edit/[Tid].tsx
. This component queries for the task’s details.
import request, { gql } from "graphql-request";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
const GetTaskByID = gql`
query GetTaskByID($taskId: String!) {
getTaskByID(id: $taskId) {
id
description
complete
tag {
id
name
}
}
}
`
const EditTask = () => {
const router = useRouter();
// Access task id from the URL
const { Tid } = router.query;
const [queryTask, setQueryTask] = useState<any>({});
const [editDescription, setEditDescription] = useState("");
const [editTag, setEditTag] = useState("");
useEffect(() => {
request({
url: "/api/graphql",
document: GetTaskByID,
variables: { taskId: Tid },
})
.then((res) => {
setQueryTask(res.getTaskByID);
console.log("Task ID ", Tid);
})
.catch(console.log);
}, [Tid]);
return (...)
}
Adding All Mutations
Implementing mutations is almost similar to setting up the queries. You will start by defining type mutation to add a task to our database.
Since you are using dummy data, any mutations we make to our data will only be saved in memory. You’ll create 3 mutations: addTask
, updateTask
and deleteTask
addTask
typeDefs: /* GraphQL */ `
(...other types)
type Mutation {
addTask(description:String!, tagName :String!):Task!
}
`
And it’s corresponding resolver:
resolvers: {
//(...other resolvers)
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 has been imported from tasks.json
tasks_data.push(newTask);
return newTask;
},
}
Our mutation adds a new task to our database and also returns the newly added task.
Add this mutation to the index.tsx
file to dynamically add a task:
const AddTask = gql`
mutation AddTask($newDescription: String!, $newTagName: String!) {
addTask(description: $newDescription, tagName: $newTagName) {
id
description
complete
tag {
id
name
}
}
}
`;
const Home: NextPage = () => {
const [tasks, setTasks] = useState([]);
const [allTags, setAllTags] = useState([]);
const [newTag, setNewTag] = useState("");
const [newDescription, setNewDescription] = useState("");
const addTask = () => {
request({
url: "/api/graphql",
document: AddTask,
variables: {
newDescription: newDescription,
newTagName: newTag,
},
})
.then((res) => {
console.log("Added task", res);
})
.catch(console.log);
};
return (...)
}
You’ve created an addTask()
function to make a request to the API when needed.
updateTask
This mutation updates a task’s data. You’ll start with type definition:
typeDefs: /* GraphQL */ `
(...other types)
type Mutation {
addTask(description:String!, tagName :String!):Task!
updateTask(id:String!, description:String, complete: Boolean, tagName: String!):Task!
}
`
Then add its corresponding resolver:
resolvers: {
//(...other resolvers)
Mutation: {
addTask: (parent: unknown, args: { description: string; tagName: string }) => {
//...
},
updateTask: (parent: unknown, args: { id: string, description: string, complete: boolean, tagName: string }) => {
//tasks_data has been imported from tasks.json
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');
},
}
The edit task mutation will be used in the edit/[Tid].tsx
file.
const UpdateTask = gql`
mutation UpdateTask(
$taskId: String!
$taskDescription: String
$tagName: String!
) {
updateTask(
id: $taskId
description: $taskDescription
tagName: $tagName
) {
id
description
complete
tag {
id
name
}
}
}
`;
const EditTask = () => {
const router = useRouter();
const { Tid } = router.query;
const [queryTask, setQueryTask] = useState<any>({});
const [editDescription, setEditDescription] = useState("");
const [editTag, setEditTag] = useState("");
const handleEdit = () => {
request({
url: "/api/graphql",
document: UpdateTask,
variables: {
taskId: Tid,
taskDescription:
editDescription === "" ? queryTask.description : editDescription,
tagName: editTag === "" ? queryTask.tag.name : editTag,
},
})
.then((res) => {
console.log("handled Edit", res);
router.push("/");
})
.catch(console.log);
};
return (...)
}
deleteTask
Lastly, you’ll define the delete task mutation:
typeDefs: /* GraphQL */ `
(...other types)
type Mutation {
addTask(description:String!, tagName :String!):Task!
updateTask(id:String!, description:String, complete: Boolean, tagName: String!):Task!
deleteTask(id:String!):Task!
}
`
And it’s corresponding resolver:
resolvers: {
//(...other resolvers)
Mutation: {
addTask: (parent: unknown, args: { description: string; tagName: string }) => {
//...
},
updateTask: (parent: unknown, args: { id: string, description: string, complete: boolean, tagName: string }) => {
//...
},
deleteTask: (parent: unknown, args: { id: string }) => {
//tasks_data has been imported from tasks.json
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');
}
}
Deletion is added in the components/Task.tsx
file. You will create a handleDelete()
function to make a delete request to the API when needed.
const DeleteTask = gql`
mutation DeleteTask($taskId: String!) {
deleteTask(id: $taskId) {
id
description
complete
tag {
id
name
}
}
}
`;
const Task = ({ id, description, complete, tag }: taskProps) => {
const handleDelete = () => {
request({
url: "/api/graphql",
document: DeleteTask,
variables: {
taskId: id,
},
})
.then((res) => {
console.log("Just Deleted", res);
router.reload()
})
.catch(console.log);
};
return (...)
}
At this point, your graphql.ts
file will probably be looking 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
```
In Conclusion
If you’ve reached this far, congratulations! You’ve just made your first GraphQL API.
However, don’t stop here, there is more to discover in Part 2 of this series. Connect your API to a MongoDB database with Prisma like a BOSS!