How To Implement CRUD using GraphQL, NodeJS, Express, and MongoDB — (Part Two)
Hey there🙂
This article is the second part of my GraphQL series.
The first part covered the project setup and performing different kinds of read operations including fetching all resources, fetching a resource by its Id, fetching a resource/ list of resources, and nested objects within them, etc.
If you have not read it, I urge you to check it out before you continue.
As promised, in this article we will cover all you need to gain a better understanding of how to efficiently use GraphQL in your Node.js projects to implement Create
, Update
, and Delete
functionalities.
The code samples used in this article can be found in this GitHub repo.
In this article, we will look at:
- Creating a root mutation
- Creating a resource using GraphQL
- Updating a resource using GraphQL
- Deleting a resource using GraphQL
- Optimizing a long-ass root mutation (from 57 lines down to 9 lines of code😏)
Let's dive into it.
In part one of this article, we created a RootQuery
which served as the basis for the read operations we did.
For us to perform CUD-based operations (modify data), we need to create something called a Mutation
. AMutation
specifies a set of fields that represent the changes to be made to the data.
For us to get started with mutations, we will create a RootMutation
in queries.js
:
//File: queries.js
exports.RootMutation = new GraphQLObjectType({
name: "RootMutation",
description: "The Root Mutation",
fields: () => ({
})
})
This is going to serve as the basis for all the data manipulation we will be performing.
For RootMutation
to be recognized, we need to add it to our schema in index.js
:
//File: index.js
//Update imports
const { RootQuery, RootMutation } = require("./queries");
const schema = new GraphQLSchema({
query: RootQuery,
mutation: RootMutation
})
Great! We will start by looking at how to create a new resource.
HOW TO CREATE A RESOURCE USING GRAPHQL
Let's update our RootMutation
to include code for creating an owner:
//File: queries.js
//Update our imports
const {
GraphQLObjectType,
GraphQLID,
GraphQLString,
GraphQLList,
GraphQLInt,
GraphQLNonNull //add this
} = require("graphql");
exports.RootMutation = new GraphQLObjectType({
name: "RootMutation",
description: "The Root Mutation",
fields: () => ({
addOwner: {
type: OwnerType,
description: "Adds a new owner",
args: {
name: {
type: new GraphQLNonNull(GraphQLString)
},
id: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve: async (_, args) => {
const owner = { _id : args.id, name: args.name };
return await Owner.create(owner);
},
}
})
});
Here the addOwner
represents a mutation that will add a new owner to our database. The args
object holds the name
and _id
properties which we need to create a new owner.
To make the name
and _id
properties required, we wrap them within GraphQLNonNull
. GraphQLNonNull
is a type constructor that wraps another GraphQL type and creates a new type that does not allow null
values. resolve
will take the arguments we provide and create a new owner
object.
Let's try it out.
For a mutation
, we have to append the “mutation” keyword because GraphQL
automatically defaults to a query:
Let's run a query to confirm that the new owner we added is there:
Yup, there she is in all her glory! Now let's try to update our new owner.
HOW TO UPDATE A RESOURCE USING GRAPHQL
Once again, we will attack our RootMutation
. This time we will add code to update an owner:
//File: queries.js
exports.RootMutation = new GraphQLObjectType({
name: "RootMutation",
description: "The Root Mutation",
fields: () => ({
addOwner: {
type: OwnerType,
description: "Adds a new owner",
args: {
name: {
type: new GraphQLNonNull(GraphQLString)
},
id: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve: async (_, args) => {
const owner = { _id: args.id, name: args.name };
return await Owner.create(owner);
},
},
updateOwner: {
type: OwnerType,
description: "Updates an owner",
args: {
name: {
type: new GraphQLNonNull(GraphQLString)
},
id: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve: async (_, args) => {
let owner = await Owner.findByIdAndUpdate(args.id, { name: args.name });
if (!owner) {
throw new Error(`Could not find owner with id: ${args.id}`);
}
return owner;
},
},
})
});
We added updateOwner
which represents a mutation that will update an existing owner. Notice how similar this is with addOwner.
By now we should have started noticing a similar pattern😎.
DO NOT FORGET TO REFRESH BEFORE TRYING OUT THE QUERY.
Alright, let's try it out:
Test that our update worked as expected:
Before looking at the next section, I challenge you to try and add the mutation for deleting an owner by yourself🙂
The final part of our CUD operation is the delete functionality.
HOW TO DELETE A RESOURCE USING GRAPHQL
We will once again update the RootMutation
to include code for deleting an owner by Id:
//File: queries.js
exports.RootMutation = new GraphQLObjectType({
name: "RootMutation",
description: "The Root Mutation",
fields: () => ({
addOwner: {
type: OwnerType,
description: "Adds a new owner",
args: {
name: {
type: new GraphQLNonNull(GraphQLString)
},
id: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve: async (_, args) => {
const owner = { _id: args.id, name: args.name };
return await Owner.create(owner);
},
},
updateOwner: {
type: OwnerType,
description: "Updates an owner",
args: {
name: {
type: new GraphQLNonNull(GraphQLString)
},
id: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve: async (_, args) => {
let owner = await Owner.findByIdAndUpdate(args.id, { name: args.name });
if (!owner) {
throw new Error(`Could not find owner with id: ${args.id}`);
}
return owner;
},
},
deleteOwner: {
type: OwnerType,
description: "Delete an owner",
args: {
id: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve: async (_, args) => {
return await Owner.findByIdAndDelete(args.id);
},
}
})
});
This is one long piece of code 😬😬.
It's the same snippet from earlier, we just added deleteOwner
to handle deleting an owner.
DO NOT FORGET TO REFRESH BEFORE TRYING OUT THE QUERY.
Let's try it out:
Once again, to confirm that our delete functionality worked:
We have looked at how to CUD resources using GraphQL.
Even though this works beautifully, notice how long our RootMutation
is?
By the time we add the CUD functionality for lands, the code will be super difficult to read and maintain.
Let's modularize it so that anyone who finds themselves in our codebase will not haunt us in our sleep. Remember “Clean code always!”.
Find the code for this article here
OPTIMIZING THE ROOT MUTATION FOR BETTER READABILITY
We will add the updated code inside a new file. Let's call it mutation.js
.
The idea is to abstract deleteOwner
, addOwner,
and updateOwner
into their separate functions and then pass these functions to our RootMutation
.
Let's start with addOwner
:
//File: mutations.js
//imports
const {
GraphQLObjectType,
GraphQLString,
GraphQLNonNull,
GraphQLInputObjectType
} = require("graphql");
const { OwnerType } = require("./queries");
const { Owner } = require("./database");
//Abstract The Input required to create an owner
const OwnerInputType = new GraphQLInputObjectType({
name: 'OwnerInput',
description: 'Fields required to create a new owner',
fields: () => ({
name: { type: new GraphQLNonNull(GraphQLString) },
_id: { type: new GraphQLNonNull(GraphQLString) }
})
});
//Abstract AddOwner functionality
const AddOwnerMutation = {
type: OwnerType,
args: {
input: { type: new GraphQLNonNull(OwnerInputType) }
},
resolve: async (_, { input }) => {
return await Owner.create(input);
}
};
//Add AddOwnerMutation to the RootMutation
exports.RootMutation = new GraphQLObjectType({
name: 'RootMutation',
description: 'The root mutation',
fields: () => ({
addOwner: AddOwnerMutation, //added this here
})
});
Here we simply abstracted the properties needed to create an owner entity by creating an GraphQLInputObjectType
object. We then abstracted AddOwnerMutation
and passed in the GraphQLInputObjectType
object as input.
Finally, we registered AddOwnerMutation
to the RootMutation
.
Next, lets updateOwner
:
//File: mutations.js
//Abstract UpdateOwner functionality
const updateOwner = async (id, name) => {
let owner = await Owner.findByIdAndUpdate(id, { name });
if (!owner) {
throw new Error(`Could not find owner with id: ${args.id}`);
}
return owner;
};
const UpdateOwnerMutation = {
type: OwnerType,
args: {
input: { type: new GraphQLNonNull(OwnerInputType) }
},
resolve: async (_, { input }) => {
return await updateOwner(input._id, input.name);
},
};
//Add UpdateOwnerMutation to the RootMutation
exports.RootMutation = new GraphQLObjectType({
name: 'RootMutation',
description: 'The root mutation',
fields: () => ({
addOwner: AddOwnerMutation,
updateOwner: UpdateOwnerMutation //added this here
})
});
Finally for deleteOwner
:
//File: mutations.js
//Abstract DeleteOwner functionality
const DeleteOwnerMutation = {
type: OwnerType,
args: {
id: { type: new GraphQLNonNull(GraphQLString) }
},
resolve: async (_, { id }) => {
return await Owner.findByIdAndDelete(id);
}
}
//Add DeleteOwnerMutation to the RootMutation
exports.RootMutation = new GraphQLObjectType({
name: 'RootMutation',
description: 'The root mutation',
fields: () => ({
addOwner: AddOwnerMutation,
updateOwner: UpdateOwnerMutation,
deleteOwner: DeleteOwnerMutation //added this here
})
});
We have reduced RootMutation
to Nine(9) lines of code and it is a beauty to behold💥
//File: mutations.js
exports.RootMutation = new GraphQLObjectType({
name: 'RootMutation',
description: 'The root mutation',
fields: () => ({
addOwner: AddOwnerMutation,
updateOwner: UpdateOwnerMutation,
deleteOwner: DeleteOwnerMutation
})
});
To make use of our optimized RootMutation
, we will simply update the import statement in index.js:
//File: index.js
//Update import
const { RootQuery } = require("./queries");
const { RootMutation } = require("./mutation");
It's been a long way here and I hope you learned a lot. Thanks for reading.
Check out my other articles.
Don’t forget to stay jiggy🙂