How To Implement CRUD using GraphQL, NodeJS, Express, and MongoDB — (Part One)

Ogubuike Alexandra
11 min readApr 12, 2023

--

GraphQL is a specification built around HTTP for how we set and receive resources from a server. Right now, the most popular way to interact with a server is via REST architecture.

With REST, we use different endpoints for retrieving and manipulating resources. GraphQL on the other hand, gets rid of the idea of using different endpoints and gives us a single endpoint for all. The way we determine what data we get back from that endpoint is based on the query that we send to it.

In this article, we are going to walk through a simplified step-by-step guide to implementing CRUD using GraphQL in NodeJS.

To demonstrate this, we will build a straightforward property management system. In this system, an owner has a list of lands that they own, and each land has only one owner.

The code samples used in this article are in this GitHub repo.

The first part of the article will cover:

  • Project setup
  • Connecting to MongoDB
  • Creating an express server
  • Creating a GraphQL Root Query
  • Creating a GraphQL Schema
  • Retrieving a list of all the owners in our system
  • Retrieving a list of all the lands in our system
  • Retrieving a land using its Id
  • Retrieving an owner using their Id
  • Using one query to get a land and its owner
  • Using one query to get an owner and a list of all the lands he owns
  • Using one query to get all the lands in the system together with the details of their owners

The second part of this article will focus on creating, updating, and deleting resources. Link to part two is here

Let's dive in.

SETUP PROJECT

To set the engine running, let’s set up our express server.

First, we will create a new folder:

mkdir graphql-project
cd graphql-project

Inside this new folder, we initialize a package.json file:

npm init -y

Next, we install our dependencies:

npm i express mongoose express-graphql graphql nodemon

Next, let's set up our database configuration.

CONNECT DATABASE

We will be using MongoDB and Mongoose. If you are not already familiar with these terms, here is an introductory guide to help out.

For proper separation of concern, we’ll add the code for configuring our database in a file we’ll call database.js:

//File: database.js

const mongoose = require("mongoose");
const Schema = mongoose.Schema;

//Connect to DB
const connectDB = () => {
mongoose
.connect(
"mongodb://127.0.0.1:27017/property-management-system"
)
.then(() => {
console.log("Connected to database...");
})
.catch((error) => {
console.error(error);
throw error;
});
};

//Set up Schema
const landSchema = new Schema({
_id: String,
location: String,
sizeInFeet: Number,
ownerId: String,
});

const ownerSchema = new Schema({
_id: String,
name: String
});

const Land = mongoose.model("Land", landSchema);
const Owner = mongoose.model("Owner", ownerSchema);

module.exports = {
Land,
Owner,
connectDB,
};

Notice, here we are using a local MongoDB database, feel free to use any MongoDB database of your choice (as long as you can connect to it🙂). If you are having issues connecting to the local MongoDB database, check out this guide on how to do so.

The connectDBfunction will try to establish a connection with our MongoDB database. If the connection is successful, it will send a message to our console. If it is not, it will send an error to the console.

CREATE EXPRESS APPLICATION

Next, let’s create an index.js file to hold the code for our server. To keep the focus on the topic we will add code for a minimal server inside index.js:

//File: index.js

const express = require("express");
const { graphqlHTTP } = require("express-graphql");
const { connectDB } = require("./database");
const app = express();

//Conect to mongoDB
connectDB();

app.use(
"/graphql",
graphqlHTTP({
graphiql: true,
})
);

app.get("/", (_, res) =>
res.status(200).send({
message: "Welcome, graphQL genius!",
})
);

app.listen(7010, () => {
console.log("Listening on 7010");
});

Let's go over what's happening here.

First, we imported our dependencies and connected our Database.
After that, we set up a GraphQL server using the graphqlHTTP middleware provided by express-graphql.

Also, notice that we defined a boolean property named graphiql . This determines whether the GraphiQL tool is enabled or not. GraphiQL is a web-based IDE that will allow us to test our GraphQL API.

Then we set up app.listen()to start the server and begin listening for incoming requests on port 7010.

Let's start the server to confirm its working:

nodemon index.js

We should be greeted by this message in our terminal:

If we try out the /graphql route from our browser, we’ll get this error:

This is because we did not set up a schema with the graphqlHTTP.

Uhm! What is a schema😕?
In simple terms, a schema is like a blueprint for our GraphQL API. It’s like a map that shows what data is available and how we can access or manipulate it.

How to Define a GraphQL Schema

We will start by creating a starting point for executing all our queries. This starting point is our root query.

It defines the available queries that can be made against our API.
In our root folder, we will create a new file — queries.js:

//File: queries.js

//update imports
const {
GraphQLObjectType,
GraphQLID,
GraphQLString,
GraphQLList
} = require("graphql");

exports.RootQuery = new GraphQLObjectType({
name: "RootQuery",
description: "The Root Query",
fields: () => ({
lands: {},
owners: {}
}),
});

Here we define a GraphQL object type called RootQuery, which represents the root query object for our API. We have also set the name and description for the GraphQL object.

After the description property, we have the fields property — The fields property is a function that returns an object containing the actual fields we will be querying defined as key-value pairs.

Right now both lands and owners are set to empty objects. To make them valid we have to create a GraphQL object type for each of them with the appropriate fields and data types.

Let's add a GraphQLObjectType that defines a land:

//File: queries.js

//imports here

const LandType = new GraphQLObjectType({
name: "Land",
description: "A property owned by a landlord",
fields: () => ({
id: { type: GraphQLID },
location: { type: GraphQLString },
sizeInFeet: { type: GraphQLString },
ownerId: { type: GraphQLID },
}),
});

//RootQuery code here

We declared a name and a description to properly distinguish the land object. The fields property returns an object that holds the attributes that define a Land.

Let’s declare a GraphQL type for the owner entity:

//File: queries.js

//LandType here

const OwnerType = new GraphQLObjectType({
name: "Owner",
description: "The owner of a property",
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString }
}),
});

//RootQuery code here

At the top of the queries.js let’s import our mongoose schemas:

const { Land, Owner } = require("./database");

Next up, we will update the RootQuery we defined earlier:

//File: queries.js

//Other part of code

exports.RootQuery = new GraphQLObjectType({
name: "RootQuery",
description: "The Root Query",
fields: () => ({
lands: {
type: new GraphQLList(LandType),
description: "A list of lands",
resolve: async () => {
return await Land.find();
},
},
owners: {
type: new GraphQLList(OwnerType),
description: "A list of owners",
resolve: async () => {
return await Owner.find();
},
},
}),
});

Let's look over what's happening in this snippet. Using the resolve function lands returns a list of lands and owners returns a list of owners.

When a client sends a query for lands or owners in a GraphQL query, our resolve function will be invoked to retrieve the data and return it to the client.

Now in index.js we will create the schema object using our RootQueryand pass the schema to our GraphQL middleware:

//File: index.js

//Update our imports
const { RootQuery } = require("./queries");
const { GraphQLSchema } = require("graphql");

// other code

//create the schema object using our RootQuery
const schema = new GraphQLSchema({
query: RootQuery
})

//pass the schema to our GraphQL middleware:
app.use(
"/graphql",
graphqlHTTP({
graphiql: true,
schema
})
);

// other code

Now when we go to localhost:7010/graphql we should see:

When we click on docs in the right-hand corner, we see our RootQuery at the bottom:

SEED INITIAL DATA INTO OUR DATABASE

To try out our root query, let's seed some data into our database. To “seed” data means to insert initial or default data into our database to initialize it for use.

We’ll create a new file seeddata.js and then we will add the code to it:

//File: seeddata.js

const { Land, Owner } = require("./database");

const owners = [{
name: "Ada Doe",
_id: "ada@mail.com"
}, {
name: "Ozioma Doe",
_id: "ozioma@mail.com"
}];

const lands = [
{
_id: 1,
location: "Uyo",
sizeInFeet: "100",
ownerId: "ozioma@mail.com",
},
{
_id: 2,
location: "Lagos",
sizeInFeet: "200",
ownerId: "ozioma@mail.com",
}, {
_id: 3,
location: "Enugu",
sizeInFeet: "300",
ownerId: "ozioma@mail.com",
}, {
_id: 4,
location: "Imo",
sizeInFeet: "400",
ownerId: "ada@mail.com",
},
{
_id: 5,
location: "Oyo",
sizeInFeet: "500",
ownerId: "ada@mail.com",
},
]

exports.seedData = async () => {
const ONE = 1;
if ((await Owner.find()).length < ONE) {
owners.forEach(async owner => {
await (await Owner.create(owner)).save();
});
}

if ((await Land.find()).length < ONE) {
lands.forEach(async land => {
await (await Land.create(land)).save();
});
}
}

Then, in our index.js, we will initialize this after connecting to our database:

//File: index.js

// imports
const { seedData } = require("./seeddata");

//Conect to mongoDB
connectDB();
seedData();

// other code

GET A LIST OF RESOURCES

The beauty of GraphQL is we get to compose a query that returns the exact data we need.

How🤔? If we want to retrieve a list of all the lands we currently have in our system together with all the properties found in our land model, we will write a query like this:

query{
lands{
location,
id,
sizeInFeet,
ownerId
}
}

The result:

What if we wanted just the location of the lands?

query{
lands{
location
}
}

The result:

Cool! We basically get back any property we pass into the query.
To retrieve the names of the landowners, our query will look like this:

Awesome right?
Let's take this a little further. Right now we have queries that fetch a list of resources, what if we wanted to fetch a single land or a single owner by their Id?

GET A SINGLE RESOURCE BY ID

To get a single resource, we will update RootQuery to:

//File: queries.js

exports.RootQuery = new GraphQLObjectType({
name: "RootQuery",
description: "The Root Query",
fields: () => ({
lands: {
type: new GraphQLList(LandType),
description: "A list of lands",
resolve: async () => {
return await Land.find();
},
},
owners: {
type: new GraphQLList(OwnerType),
description: "A list of owners",
resolve: async () => {
return await Owner.find();
},
},
land:{
type: LandType,
description: "A single land",
args: {
id: {
type: GraphQLID,
},
},
resolve: async (_, args) => {
return await Land.findById(args.id);
},
},
owner:{
type: OwnerType,
description: "A single owner",
args: {
id: {
type: GraphQLID,
},
},
resolve: async (_, args) => {
return await Owner.findById(args.id);
},
}
}),
});

Notice we added two new parts to fieldland and owner— which will return a single resource based on its ID.

The args object is used to store the parameter we want to pass into a GraphQL query. Here it defines a single argument called "id", which is of type GraphQLID. We use this argument to specify the ID of the resource we wanna retrieve.

resolve takes in the parent object and the args object. We represent the parent object using _ , because we are not using it inside resolve.

When land or owner is queried with a valid “id” argument, resolve will be invoked, and it will retrieve and return the corresponding resource. If the specified ID does not correspond to any item in our DB, our function will return null.

Let's test this out.
We can simply copy one of the ids from our initial query that returned all the resources. To get the location of a land document using its id:

query{
land(id: "2"){
location
}
}

This returns:

In the same way, if we wanted to get all the properties in the land object we will have:

Cool!

GET NESTED OBJECTS FROM A SINGLE QUERY

To finalize this let’s look at how we can get an owner and all his lands.

In queries.js, we will update the fields property of the OwnerType :

//File: queries.js

const OwnerType = new GraphQLObjectType({
name: "Owner",
description: "An owner of a property",
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
lands: {
type: new GraphQLList(LandType),
description: "Owner's land",
resolve: async (owner) => {
return Land.find({ ownerId: owner.id });
}
}
}),
});

Here, we added lands which is a list of LandType. When lands is queried, resolve will be invoked, and it will retrieve a list of all the lands owned by the Owner . Notice that the resolve function takes in the parent object, which is the owner we are querying at the moment.

We can do the same thing for our LandType so that we will be able to get a land and its owner in one call:

//File: queries.js

//update import
const {
GraphQLObjectType,
GraphQLID,
GraphQLString,
GraphQLList,
GraphQLInt
} = require("graphql");

const LandType = new GraphQLObjectType({
name: "Land",
description: "A property owned by a landlord",
fields: () => ({
id: { type: GraphQLID },
location: { type: GraphQLString },
sizeInFeet: { type: GraphQLInt },
ownerId: { type: GraphQLID },
owner: {
type: OwnerType,
resolve: (land) => {
return Owner.findById(land.ownerId);
},
},
}),
});

Here notice that owner is a single OwnerType and not a list of OwnerType. This is because a land can only belong to one owner. Let’s start our server and test this out.
To fetch a land and its owner’s name:

To get all the lands in the system together with the owner details:

To get an Owner and the details for his lands:

This is just the tip of the Iceberg. With this, we can begin to imagine the many crazy things we can do with GraphQL.

In this article, we have done only read operations and in the next article, we will take our property management system one step further by adding the Create, Update, and Delete functionalities.

Here is a link to part two

Don't forget to stay jiggy!

--

--

Ogubuike Alexandra

Founder @ Codetivite | Senior Backend Engineer | Technical Writer / OpenSource Contributor @ CodeMaze