🍕
Published on

PART 1: Building Your First GraphQL API with Next.js

Authors

PART 1: Building Your First GraphQL API with Next.js.

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

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.

complete-todo-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.

plain-playground

A simple query

In the Yoga GraphiQL playground, add the following query:

 query {
    hello
  }

hello-playground

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.

console-hello

Now that you have the basics laid down, let’s kick it up a notch!

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

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 called parent): 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 the root argument in each resolver call is simply the result of the previous call (initial value is null if not otherwise specified).

args: This argument carries the parameters for the query, for example, the id or description of a Task 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. Get-All-Tasks-with-tags

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. getalltags-hard-code-done.png

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:

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.

https://media.giphy.com/media/ely3apij36BJhoZ234/giphy-downsized-large.gif

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!

Further Reading...