DEV Community

Dan
Dan

Posted on

React TodoMVC with Apollo - Server

Photo by Md Mahdi on Unsplash
In this tutorial I will share how to build a simple TodoMVC with Apollo server and Apollo client 3. We will cover the basics of apollo server and client by build a React TodoMVC app.
This is part one of the tutorial - build backend with Apollo server.

  • 1. React TodoMVC with Apollo - Server
  • 2. React TodoMVC with Apollo - Client (WIP)
  • 3. React TodoMVC with Apollo - Test (WIP)
  • 4. React TodoMVC with Apollo - Deployment (WIP)

Prerequisite

If you are new to React or Apollo/Graphql, make sure you go through links below first to get a gist.

Without further ado lets get started!

installation

Create our project folder:

mkdir todomvc-apollo-server
cd todomvc-apollo-server

First, create a package.json contains libraries we need for our project:

{
  "name": "todomvc-apollo-server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "nodemon --exec babel-node src/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.10.5",
    "@babel/core": "^7.11.4",
    "@babel/node": "^7.10.5",
    "@babel/preset-env": "^7.11.0",
    "nodemon": "^2.0.4"
  },
  "dependencies": {
    "apollo-server": "^2.17.0",
    "graphql": "^15.3.0"
  }
}

Install dependencies:

npm i

Config babel

To use new javascript features we need to config babel by creating a .babelrc with content:

{
  "presets": ["@babel/preset-env"]
}

You can use npm-check-updates to update to latest versions.

Implementation

Next stop lets implement a basic apollo server with an api that returns our todo list.

1 - First Graphql Query

schema

A schema is where we define our types, query, mutation and subscription. In our tutorial, we have a TODO type, and a todos Query. That means as a client when I query todos, I expect a array of TODO as response. Exclamation mark means the value can not be null (I.e. Your resolver should not return null for this type). Read more on : https://graphql.org/learn/schema/#lists-and-non-null.

Create typeDefs.js with following content.

const { gql } = require("apollo-server");

export const typeDefs = gql`
  type Query {
    todos: [TODO!]!
  }
  type TODO {
    id: ID!
    text: String!
    completed: Boolean!
  }
`;

resolvers

Create resolvers.js with following content.

Simply put, resolver defines what we should do for a query/mutation/subscription. In this case, we simply returns an array of TODO for Query todos.

export const resolvers = {
  Query: {
    todos: () => [
      {
        id: "0",
        text: "Buy milk",
        completed: false,
      },
      {
        id: "1",
        text: "Sing a song",
        completed: true,
      },
    ],
  },
};

Index.js

Create index.js, where we start our apollo server.

import { ApolloServer } from "apollo-server";
import { typeDefs } from "./typeDefs";
import { resolvers } from "./resolvers";

const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
  console.log(`πŸš€  Server ready at ${url}`);
});

Now we finished our basic apollo server. run npm start then open http://localhost:4000, you should be able to see apollo playground running! πŸš€

Try the query below you should be able to see apollo return our todo list at right side! 🐸

query todos {
  todos {
    id
    text
    completed
  }
}

You can find code we had so far in here.

https://github.com/bluedusk/todomvc-apollo/tree/server/1-basic-apollo-server

2 - Mutations

Mock datasource

Before we start add mutations to our server. We need to create a mock datasource to provide basic todo CRUD functions, create a datasource.js as blow:

/**
 * A mock datasource providing todo CRUD functionalities
 */
export class Todos {
  constructor() {
    this.todos = [
      {
        id: "0",
        text: "Buy milk",
        completed: false,
      },
      {
        id: "1",
        text: "Sing a song",
        completed: true,
      },
    ];
  }

  getTodos() {
    return this.todos;
  }

  setTodos(todos) {
    this.todos = todos;
  }

  addTodo(todoText) {
    const todo = {
      id: String(this.getTodos().length + 1),
      text: todoText,
      completed: false,
    };
    this.todos.push(todo);
    return todo;
  }

  deleteTodo(id) {
    const todo = this.todos.find((todo) => todo.id === id);
    if (todo) {
      this.todos = this.todos.filter((todo) => todo.id !== id);
    }
    return todo;
  }

  updateTodoById(id, text) {
    let result;
    this.todos.forEach((todo) => {
      if (todo.id === id) {
        // update text or completed
        if (text) {
          todo.text = text;
        } else {
          todo.completed = !todo.completed;
        }
        result = todo;
      }
    });

    return result;
  }
  deleteAll() {
    this.todos = [];
  }
  deleteCompleted() {
    this.todos = [...this.todos].filter(({ completed }) => !completed);
  }
  completeAll() {
    this.todos = [...this.todos].map((todo) => {
      return {
        ...todo,
        completed: true,
      };
    });
  }
}

Update typeDefs

Add following mutation to typeDefs.js

  type Mutation {
    addTodo(text: String!): TODO!
    updateTodo(id: ID!, text: String): TODO!
    deleteTodo(id: ID!): TODO
    completeAll: Boolean
    deleteCompleted: Boolean
  }

Update resolvers

Replace resolvers.js with following updates. Notice that how we get arguments and context in resolver functions.

export const resolvers = {
  Query: {
    todos: (parent, args, { Todos }) => {
      return Todos.getTodos();
    },
  },
  Mutation: {
    addTodo: (parent, { text }, { Todos }) => {
      const result = Todos.addTodo(text);
      return result;
    },
    deleteTodo: (parent, { id }, { Todos }) => {
      const result = Todos.deleteTodo(id);
      return result;
    },
    updateTodo: (parent, { id, text }, { Todos }) => {
      const result = Todos.updateTodoById(id, text);
      return result;
    },
    deleteCompleted: (_, __, { Todos }) => {
      Todos.deleteCompleted();
      return true;
    },
    completeAll: (_, __, { Todos }) => {
      Todos.completeAll();
      return true;
    },
  },
};

Update index.js

Replace index.js with following updates. Notice that we injected Todos to context so that it can be used in each resolver method in last section. In our example we want todos object shared across multiple queries, so context is an object not a function such that it will not be triggered for each request. In reality, we might want that for each request we want new context, we can set context to be a funcion: ()=> { /***/}

More on context: https://www.apollographql.com/docs/apollo-server/data/resolvers/#the-context-argument

import { ApolloServer } from "apollo-server";
import { typeDefs } from "./typeDefs";
import { resolvers } from "./resolvers";
import { Todos } from "./datasource";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  // context: Where we "inject" our fake datasource
  context: {
    Todos: new Todos(),
  },
  // plugins(optional): A small plugin to print log when server receives request
  // More on plugins: https://www.apollographql.com/docs/apollo-server/integrations/plugins/
  plugins: [
    {
      requestDidStart(requestContext) {
        console.log(
          `[${new Date().toISOString()}] - Graphql operationName:  ${
            requestContext.request.operationName
          }`
        );
      },
    },
  ],
  // capture errors
  formatError: (err) => {
    console.log(err);
  },
});

// The `listen` method launches a web server at localhost:4000.
server.listen().then(({ url }) => {
  console.log(`πŸš€  Server ready at ${url}`);
});

To avoid playground IntrospectionQuery spam your console, change playground setting as blow:

  "schema.polling.enable": false,

Thats it! make sure your server is running and check schema.test.graphql in the repo, you can now try some mutation in playground!

You can find code we had so far in here.

https://github.com/bluedusk/todomvc-apollo/tree/server/2-mutations

Subscription

Our next mission is to play around with graphql subscription.
Subscription comes in handy when we want to receive updates via server push, rather than constantly polling server from client. (Note that polling might be good enough for some usecases, providing Apollo supports dynamic polling which allows you to start/stop and set polling interval dynamiclly. https://www.apollographql.com/docs/react/data/queries/#usequery-api)

Unlike queries, subscriptions maintain an active connection to your GraphQL server (most commonly via WebSocket). This enables your server to push updates to the subscription's result over time.
https://www.apollographql.com/docs/react/data/subscriptions/

schema

Add submission type to typeDefs.js, like Query and Mutation, Subscription is another root type.

  type Subscription {
    todos: [TODO!]!
  }

resolver

Replace resolvers.js with content below:

import { PubSub } from "apollo-server-express";

const pubsub = new PubSub();
const TODO_CHANGED = "TODO_CHANGED";

const doPublish = (todos) => {
  pubsub.publish(TODO_CHANGED, { todos });
};

export const resolvers = {
  Query: {
    todos: (parent, args, { Todos }) => {
      return Todos.getTodos();
    },
  },
  Mutation: {
    addTodo: (parent, { text }, { Todos }) => {
      const result = Todos.addTodo(text);
      doPublish(Todos.getTodos());
      return result;
    },
    deleteTodo: (parent, { id }, { Todos }) => {
      const result = Todos.deleteTodo(id);
      doPublish(Todos.getTodos());
      return result;
    },
    updateTodo: (parent, { id, text }, { Todos }) => {
      const result = Todos.updateTodoById(id, text);
      doPublish(Todos.getTodos());
      return result;
    },
    deleteCompleted: (_, __, { Todos }) => {
      Todos.deleteCompleted();
      doPublish(Todos.getTodos());
      return true;
    },
    completeAll: (_, __, { Todos }) => {
      Todos.completeAll();
      doPublish(Todos.getTodos());
      return true;
    },
  },
  Subscription: {
    todos: {
      subscribe: () => {
        return pubsub.asyncIterator([TODO_CHANGED]);
      },
    },
  },
};

Notice that in our subscription resolver, we are listening event TODO_CHANGED:

  subscribe: () => {
    return pubsub.asyncIterator([TODO_CHANGED]);
  },

We publish this event in our mutations such as add/delete/update todo:

pubsub.publish(TODO_CHANGED, { todos });

To test subscription, we open two playground (http://localhost:4000), in first one we start subscription with :

subscription sub_todos {
  todos {
    id
    text
    completed
  }
}

You can see it start listening. In second playground we add one todo:

mutation addTodo {
  addTodo(text: "another todo") {
    id
    text
    completed
  }
}

After addTodo we can see in first playground we received the event and updated todo list! 🌟

You can find code we had so far in here.

https://github.com/bluedusk/todomvc-apollo/tree/server/3-subscriptions

End

That it for apollo server part of this tutorial. In next part we will build our todoMvc react client with apollo client. Thanks for reading!

Top comments (0)