In this tutorial, we’ll build a CRUD app with React 18.2 and Apollo GraphqL 4, This tutorial is targeted at React developers who want to dive into React graphQL. You need workable knowledge of React to follow along.

Demo of our app:


To complete this tutorial in the most effective way you need to keep some things in your mind.

  • A configured computer with an internet connection.
  • Basic knowledge of React, HTML, and CSS.
  • A text editor – You can choose any text editor on which you may write JavaScript code such as sublime text, nodepad++, etc. In this tutorial, we will use visual studio code.
  • NodeJS is installed on your computer.

A Quick Overview of GraphQL

  • GraphQL is an open-source data query and manipulation language for application programming interfaces (APIs).
  • GraphQL, unlike other API formats, allows an API consumer to request specific data from an application’s server without receiving unnecessary information.
  • In contrast, traditional REST API architectures provide a fixed data structure and then rely on clients to filter out any unnecessary information.
  • GraphQL’s design provides an advantage in terms of security. Because GraphQL does not return data that the client has not explicitly requested, its use reduces the risk of information disclosure issues.
  • GraphQL is language-independent and works with a wide range of programming languages, including JavaScript, Java, Python, and Ruby.
  • GraphQL implements a schema to define the types of data that can be queried in addition to the relationships between those types.

Let’s look at an example. Let’s say your app needs to fetch a user’s details from the server. Using REST, your response may look like this:

  user_details: {
    id: 2348,
    name: “Deven Rathore”,
    email: “[email protected]”

With GraphQL, you can request exactly the data you want and be sure you’ll get only what you asked for. Assuming what you need is just the user’s name, your response would look like this:

  user_details: {
    name: “Deven Rathore”

With GraphQL, you get just one endpoint to which you send queries and mutations to manipulate your data.

A Quick Overview of Apollo:

Apollo is a set of tools and libraries developed (by Meteor Development Group) to help developers use GraphQL in their projects. Apollo has a client and a server. 

Apollo Server:

Apollo server is a library built to ease the connection of a GraphQL scheme to an HTTP (Hypertext Transfer Protocol) server in Node.js and its supported frameworks. 

Apollo Client:

Apollo client is a library built to ease the fetching of data from GraphQL servers with JavaScript.

It is a fully-featured caching GraphQL client. It can be used with any modern JavaScript framework like Vue.js, React, Angular, and more.

Before Getting started:

Take your web development skills to the next level with the comprehensive React course offered by Pluralsight. This industry-leading learning platform offers expert-led lessons that cover the latest techniques and best practices in React. Start learning now and watch your confidence and abilities grow with each lesson.

For this tutorial, we’ll have two servers running: one serving our backend and the second serving our React CRUD application. We’ll start with the backend first.

Setup Apollo Server

From your terminal, create a new directory and install the required packages:

mkdir todo-graphql && cd todo-graphql

Inside this directory, create another folder:

mkdir server

the server will contain our GraphQL server. Switch to the server folder in your terminal:

cd server

Now, install the following:

npm install @apollo/server graphql

Create a file in the root directory called index.js and add the following:

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

let todos = [
      text: 'Hello from GraphQL',
      completed: true,

The application we’ll be creating is a to-do list application that would keep track of todos and if they have been completed or not.

We’ll store todos in an array but the array can be substituted for a database or file. To keep things simple, we’ll be using an array.

Each todo has an id, text, and completed. So far so good.

Add the following after todos:

const typeDefs = `
  type Todo {
    id: String
    text: String
    completed: Boolean
  type Query {
    todos: [Todo]!
  type Mutation {
    createTodo(text: String!):String
    removeTodo(id: String!):String
    updateTodo(id: String!):String

First, we define a schema called Todo. This is basically a model for a Todo. Next, we define our query. We have just one query todos which contains an array of Todo we defined earlier. The (!) at the end signifies that it will always return something.

You can think of a query as the way you fetch data from a GraphQL server. It’s like a GET request in REST. This query will fetch todos from the server that will be used to populate the client app.

We also define 2 mutations. A mutation is a way to update data on the server. The first mutation we have is createTodo. It accepts one parameter text, which must be a String. It then returns a string after it’s finished. The same thing goes for removeTodo.

Next, we add the resolvers.

const resolvers = {
    Query: {
      todos: () => todos,
    Mutation: {
      createTodo: (parent, args, context, info) => {
        return todos.push({
          text: args.text,
          completed: false,
      removeTodo: (parent, args, context, info) => {
        for (let i in todos) {
          if (todos[i].id === {
            todos.splice(i, 1);
      updateTodo: (parent, args, context, info) => {
        for (let i in todos) {
          if (todos[i].id === {
            todos[i].completed = !todos[i].completed;

Resolvers are the functions that run when queries and mutations are made. This is where you would fetch and manipulate the data that’s returned to the client. todos query returned the array of todos. Mutations receive 4 arguments but we are particularly interested in args, which contains the data passed from the client. We use that data to manipulate the array of todos and return a result.

const server = new ApolloServer({
  const { url } = await startStandaloneServer(server, {
    listen: { port: 4000 },
  console.log(`🚀  Server ready at: ${url}`);

Lastly, we pass the type definitions and resolvers to create our ApolloServer. We also make sure to use cors() to allow cross-origin requests. Then we serve our app on port 4000.

From the terminal, run npm start

If everything was set up correctly, you should see Now browse to http://localhost:4000/ in your terminal. That link is where we would be sending all queries and mutations from the client app.

Navigating to that URL gives you an interactive shell where you can run test your queries and mutations before integrating them into your client app.

Building our CRUD app with React

So far we’ve been able to set up the server and run queries from the interactive shell. Let’s create the React app that will make use of our API.

npx create-react-app client

This will create a new React project and install the necessary packages required to run react applications. Next, we’ll install Apollo Client.

npm install @apollo/client graphql

Open up src/index.js. Remove everything there and add the following.

import React from 'react';
import ReactDOM from "react-dom/client";
import './index.css';
import App from './App';
import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: "http://localhost:4000",
  cache: new InMemoryCache()


const root = ReactDOM.createRoot(document.getElementById('root'));        
root.render(<ApolloProvider client={client}>
  <App />

The above code sets up a React application that uses the Apollo GraphQL client to connect to a GraphQL API at the URL http://localhost:4000.

The ApolloProvider component will be used to wrap the React application and provide the ApolloClient instance to all the components that need to make GraphQL queries or mutations. The ApolloClient instance is created in the next step.

const client = new ApolloClient({ uri: "http://localhost:4000", cache: new InMemoryCache() });: creates an instance of the ApolloClient class and passes in the configuration options.

The uri option specifies the URL of the GraphQL API, and the cache option sets up an in-memory cache that can be used to store the results of GraphQL queries.

ReactDOM.createRoot(document.getElementById('root'));: uses the createRoot method from the ReactDOM library to create a root component for the application.

The document.getElementById('root') call finds the HTML element with the ID of root, which will be the location in the HTML where the React application will be rendered.

The ApolloProvider component is used to wrap the App component, and the client option is passed to the ApolloProvider to provide the ApolloClient instance to all the components in the application.

Open up src/App.js, clear everything in it and add the following:

import React, { useState, input } from 'react';
import './App.css';
import { useQuery, useMutation } from '@apollo/client';
import gql from "graphql-tag";

We imported the necessary ReactJS components, along with the useState and input hooks from ReactJS, and the useQuery and useMutation hooks from the @apollo/client library. The gql import is the graphql-tag library, which is used to write GraphQL queries in a more readable format.

Below that, add the following:

const READ_TODOS = gql`
  query todos{
    todos {

const CREATE_TODO = gql`
  mutation CreateTodo($text: String!) {
    createTodo(text: $text)

const REMOVE_TODO = gql`
  mutation RemoveTodo($id: String!) {
    removeTodo(id: $id)

const UPDATE_TODO = gql`
  mutation UpdateTodo($id: String!) {
    updateTodo(id: $id)

Here, we defined four GraphQL queries to interact with the back end:

  • READ_TODOS: reads the list of to-dos.
  • CREATE_TODO: creates a new to-do item.
  • REMOVE_TODO: removes an existing to-do item.
  • UPDATE_TODO: updates the completion status of a to-do item.

Let’s send some data back to the server using mutations.

function App() {
  const [todoText, setTodoText] = useState("");
  const { data, loading, error } = useQuery(READ_TODOS);
  const [createTodo] = useMutation(CREATE_TODO);
  const [deleteTodo] = useMutation(REMOVE_TODO);
  const [updateTodo] = useMutation(UPDATE_TODO);

  const handleSubmit = (e) => {
    createTodo({ variables: { text: todoText } });

  const handleDelete = (id) => {
    deleteTodo({ variables: { id } });

  const handleUpdate = (id, completed) => {
    updateTodo({ variables: { id, completed: !completed } });

  if (loading) return <p>loading...</p>;
  if (error) return <p>ERROR</p>;
  if (!data) return <p>Not found</p>;

In the code snippet above:

  • useState hook: The code sets up a state variable, todoText, with an initial value of an empty string, and a function setTodoText to update its value.
  • useQuery hook: The code sets up a query hook to read the list of to-dos from the back-end using the READ_TODOS query.
  • useMutation hooks: The code sets up 3 mutation hooks to interact with the back-end to create, delete and update to-do items.

The code also defines 3 event handlers to handle user interactions:

  • handleSubmit: creates a new to-do item when the form is submitted.
  • handleDelete: deletes an existing to-do item.
  • handleUpdate: updates the completion status of a to-do item.

Modify the form like so:

return (
    <div className="app">
      <div class="flex flex-col space-y-4 ...">

<h3 class="font-medium leading-tight text-3xl mt-0 mb-2 text-blue-600"> Welcome to Your To do app</h3>
      <form onSubmit={e => {
        createTodo({ variables: { text: input.value } });
        input.value = '';
        <div class="flex space-x-4 ...">

        <input className="placeholder:italic placeholder:text-slate-400 block bg-white w-full border border-slate-300 rounded-md py-2 pl-9 pr-3 shadow-sm focus:outline-none focus:border-sky-500 focus:ring-sky-500 focus:ring-1 sm:text-sm" type="text" placeholder="Enter a task or todo" ref={node => { input = node; }}></input>
        <button className="inline-block px-6 py-2 border-2 border-blue-600 text-blue-600 font-medium text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5 focus:outline-none focus:ring-0 transition duration-150 ease-in-out" type="submit">Submit</button>

      <div class="flex flex-col space-y-4 ">

        { =>
          <li key={} className="w-100">
            <div class="flex space-x-4 ...">

            <span className={todo.completed ? "done" : "pending"}>{todo.text}</span>
            <button className="inline-block px-6 py-2 border-2 border-red-600 text-red-600 font-medium text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5 focus:outline-none focus:ring-0 transition duration-150 ease-in-out float-right" onClick={() => {
              deleteTodo({ variables: { id: } });
            }}> Delete</button>
             <button className={`btn inline-block px-6 py-2 border-2 border-yellow-500 text-yellow-500 font-medium text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5 focus:outline-none focus:ring-0 transition duration-150 ease-in-out float-right ${todo.completed ? "inline-block px-6 py-2 border-2 border-green-500 text-green-500 font-medium text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5 focus:outline-none focus:ring-0 transition duration-150 ease-in-out" : "btn-info"}`} onClick={() => {
              updateTodo({ variables: { id: } });
            }}>{todo.completed ? <span> Task Completed</span> : <span>Task Not completed</span>}</button>



export default App;

The Render code checks for loading, error, and data states of the query result, and displays the appropriate messages. The UI displays a form to add a new to-do item, a list of existing to-do items, and buttons to delete and update the to-do items.

And finally, The UI is styled using Tailwind CSS classes, Here is a guide to setting up tailwind CSS in your project and we are also using the window.location.reload() method to refresh the page after adding, deleting, or updating a to-do item.


GraphQL is quite exciting as it tackles some of the issues with REST. Using Apollo GraphQL gives us the flexibility to use GraphQL with ease without too much overhead cost. By combining the powerful features of these technologies, it is possible to create a dynamic and user-friendly To-do app that can handle various tasks and manage data in a more efficient and effective way.

The integration of GraphQL and React provides a seamless data-fetching experience, while TailwindCSS provides a clean and customizable design for the app’s user interface. With these tools, developers can achieve high-quality results that meet the requirements of any modern web application.

The full Code source is on GitHub so you can check it out there.