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.
- React
- Graphql https://graphql.org/
- Apollo
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)