Photo by James Pond on Unsplash

Easy and readable React/GraphQL/Node Stack — Part 2

In Part 1 I described a simple full stack that integrates React JS with hooks, GraphQL and PostgreSQL and created simple GraphQL server that lets you list todos or get information about a single todo. In this article we will enhance the GraphQL server by adding support for nested properties and mutations. You can find the code for part 2 on Github.

The project so far just has a simple list of todo items.

Let’s introduce the idea of ‘Projects’ to our model. A project is used to group together todos so you have todos for Project A and another list of todos for Project B.

Create a new database migration to add a project table and a project id field to tasks. From the server folder:

yarn knex migrate:make project

exports.up = function(knex) {
return knex.schema
.createTable('project', project => {
project
.uuid('id')
.notNullable()
.primary()
project.string('title').notNullable()
})
.table('todo', todo => {
todo
.uuid('projectId')
.references('id')
.inTable('project')
})
}
exports.down = function(knex) {
return knex.schema
.table('todo', todo => {
todo.dropColumn('projectId')
})
.dropTable('project')
}

Lets add some seed data too

# server/src/db/seed/todo.js
const uuid = require('uuid')

async function clear(knex) {
await knex('todo').del()
}

async function seed(knex) {
await clear(knex)
  const projectId = uuid()
  await knex('project').insert({
id: projectId,
title: 'Project',
})

await knex('todo').insert({
projectId,
id: uuid(),
title: 'Test action one',
complete: false,
})
  await knex('todo').insert({
projectId,
id: uuid(),
title: 'Test action two',
complete: false,
})
}
module.exports = { clear, seed }

And update the database

yarn knex migrate:latest
yarn knex seed:run

before you can take a look at your ‘Todo’ and ask to see the ‘Project’ for it you need to tell GraphQL it exists. When you defined the ‘Todo’ type you used basic types like ‘String’ and ‘Boolean’ but in this case a ‘Todo’ has a ‘Project’ property. So let’s start off by updating the schema to say that a ‘Todo’ has a ‘Project’, and at the same time tell it a ‘Project’ has many ‘Todo’ values.

# server/server/graphql/schema.graphql
type Project {
id: ID!
title: String!
todos: [Todo]
}
type Todo {
id: ID!
title: String!
complete: Boolean!
project: Project
}
type Query {
projects: [Project]
project(id: ID!): Project

todos: [Todo]
todo(id: ID!): Todo
}
type Mutation {
addProject(title: String!): Project!
updateProject(id: ID!, title: String!): Project
addTodo(title: String!): Todo!
updateTodo(id: ID!, title: String!, complete: Boolean!): Todo!
}

Now, if you were to try and see a ‘Project’ for a ‘Todo’ by starting the server and updating the ‘Query’ you’d be disappointed. It will just tell you the ‘Project’ is null and it’s not. When Apollo Server gets the data from the database and looks at the schema and query to figure out what to return it looks for a resolver for that type and if it doesn’t find one it uses a default one that just returns the fields from the database. In this case there is a ‘project’ property but no ‘project’ field in the database so it just says null. In order to intelligently figure out how to turn database data into GraphQL data you need to write a resolver for each type. So we need to write a resolver for the ‘Todo’ type and then another for the ‘Project’ type. Let’s do the ‘Todo’ resolver.

# server/src/graphql/resolvers/Todo.js
const knex = require('../../db/knex')
module.exports = {
id: obj => obj.id,
title: obj => obj.title,
complete: obj => obj.complete,
project: obj =>
knex('project')
.where({ id: obj.projectId })
.first(),
}

# server/src/graphql/index.js
const Query = require('./Query')
const Todo = require('./resolvers/Todo')
const resolvers = {
Query,
Todo,
}
module.exports = { resolvers }

A type resolver returns an object/map that provides a function for each property in the type. The function signature for each field is:obj, args, context, info. In the case of most type property resolvers you are only interested in obj which in our case is the data returned from the database. For most of the fields we just need to return the original value from the database with the exception of the ‘project’. When we want a ‘project’ property what we actually want is to use the ‘projectId’ from the database record and lookup a record in the ‘project’ table. After that we just add the resolver for ‘Todo’ to the collection of resolvers, like we did for Query.

Now fire up the playground and build a query to ask for the ‘Project’ title, if you don’t know GraphQL well enough yet just copy from the query in the screenshot.

And if we want to do the same in reverse from ‘Project’.

const knex = require('../../db/knex')
module.exports = {
id: obj => obj.id,
title: obj => obj.title,
todos: obj => knex('todo').where({ projectId: obj.id }),
}

You will also need Query resolvers for ‘project’ and 'projects’. I’m going to let you do that, just cut and paste the ones created for ‘todo’ and ‘todos’.

Go back to your playground and

Finally mutations, how do you add a new project? When we created the Query resolvers we put them into a folder named ‘Query’ and built a map with ‘resolvers.Query.queryName’ so let’s do the same with ‘Mutation’.

# server/src/graphql/Mutation/addProject.js
const uuid = require('uuid')
const knex = require('../../db/knex')
const addProject = async (_, args) => {
const id = uuid()
const project = { id, ...args }
await knex('project').insert(project)
return project
}
module.exports = addProject


# server/src/graphql/Mutation/index.js
const addProject = require('./addProject')
const Mutation = { addProject }
module.exports = Mutation
# server/src/graphql/index.js
const Mutation = require('./Mutation')
const Query = require('./Query')
const Project = require('./resolvers/Project')
const Todo = require('./resolvers/Todo')
const resolvers = {
Mutation,
Project,
Query,
Todo,
}
module.exports = { resolvers }

Mutation resolvers use the same signature as type resolvers. In this case we don’t want the original object, there isn’t one, we want the arguments sent to the resolver. The mutation function then creates a record in the database using Knex and returns that object which in turn will be processed by the type resolver.

With regards to nested mutations I’ve not come across the need to use them. In theory they should work but your mileage may vary. The addTodo mutations is a little different as it raises the question of how you specify the project you want to associate the todo with? In this example we’ll follow a convention of sending the project as ‘projectId’ which matches the field name in the table we use and also seems a good match for ‘project.id’. It also makes writing the mutation no different

const uuid = require('uuid')
const knex = require('../../db/knex')
const addTodo = async (_, args) => {
const id = uuid()
const todo = { id, ...args }
await knex('todo').insert(todo)
return todo
}
module.exports = addTodo

Feel free to finish things up and write the update mutations and wire them up.

We now have a complete GraphQL API including nested objects and mutations. The next article in this series moves onto the front-end and describes how we can use react and start using data.

As always the code for this part in the series can be found on Github