GraphQL and Node js — Part 1

Tókos Bence
14 min readJan 15, 2024

--

GraphQL and Node Js

Introduction

GraphQL and Node.js make a dynamic duo in modern web development. GraphQL is a query language for your API, while Node.js is a robust runtime environment for server-side applications. When combined, they empower developers to create efficient, flexible, and real-time communication between clients and servers.

The advantage of using GraphQL with Node.js lies in its ability to provide a single endpoint for clients to request precisely the data they need, minimizing over-fetching and under-fetching of information. This seamless synergy enables developers to build powerful, customizable APIs and simplifies data communication by allowing clients to specify their data requirements through queries and mutations, ensuring a more efficient and responsive backend experience.

Only one endpoint! Sounds cool, right? So why wait any longer? Let’s start developing!

First, we create the base node app with the following command:

npm init 

After we initialized our starting app, let’s install the necessary node packages:

npm install express

Express module is a web application framework that simplifies the process of building web servers and APIs. It offers a wide range of features, including routing, middleware support, and a straightforward API for handling HTTP requests and responses. Express is known for its flexibility, allowing developers to create both simple single-page applications and complex, enterprise-grade projects with ease.

Let’s create our app in the index.js:

const express = require("express");
const app = express();
const PORT = 5000;

app.listen(PORT, () => {
console.log("Server running");
});

To run our app just type node index.js and our application is running on http//:localhost:5000/

Now we install the other necessary libraries and along the way you would see what these packages doing.

We will work with Mongo DB so if you not have installed on your machine then follow the instructions from here:

https://www.mongodb.com/docs/manual/installation/ .

After you have installed on your machine then install the mongoose package with we can interact between the node js and the database.

npm install mongoose

MongoDB is a popular NoSQL database management system known for its flexibility and scalability. It stores data in a document-oriented format, using BSON (Binary JSON). It is widely used in web and mobile applications, offering developers the ability to work with data in a more dynamic and agile manner compared to traditional relational databases. Our first task is to create a connection between our application and the database.

Edit the index.js:

const express = require("express");
const app = express();
const mongoose = require("mongoose");
const PORT = 5000;

mongoose
.connect("mongodb://127.0.0.1/{Your db collection name}")
.then(() => console.log("Connected to MongoDB..."))
.catch((err) => console.error("Could not connect to MongoDB..."));

app.listen(PORT, () => {
console.log("Server running");
});

So after we have the initial app and we connected to a db we can kick off our journey with GraphQL. Let’s install the necessary packages:

npm install express-graphql graphql

Express-GraphQL is a middleware package for Node.js and Express.js that simplifies the process of creating a GraphQL API. It provides an easy way to expose a GraphQL endpoint in an Express application.

Key Features

  • Middleware Integration: Express-GraphQL seamlessly integrates with Express.js, making it a popular choice for adding GraphQL functionality to existing Express applications.
  • Schema Definition: You can define your GraphQL schema using the GraphQL.js schema language, and Express-GraphQL helps in routing requests to the appropriate schema.
  • Data Resolution: It allows you to define resolvers that specify how to fetch data from your data sources, whether it’s a database or other APIs.
  • Query Validation: Express-GraphQL automatically validates and executes GraphQL queries and provides detailed error messages.

GraphQL is a query language and runtime for APIs. It was developed by Facebook and is designed to provide a more efficient, powerful, and flexible alternative to the traditional REST API.

Key Features

  • Client-Defined Queries: With GraphQL, clients can request exactly the data they need, avoiding over-fetching or under-fetching of data, which is common in REST.
  • Strongly Typed: GraphQL APIs are strongly typed. You define the types, queries, and mutations, which helps in catching errors early.
  • Single Endpoint: GraphQL typically has a single endpoint for all requests, making it efficient and reducing the number of network requests.
  • Real-time Data: GraphQL supports real-time data using subscriptions, enabling features like live updates and chat functionality.
  • Introspection: You can introspect a GraphQL schema to understand its capabilities and structure.

For the better project organization create a folder Schema, inside this folder, we create a folder for our model types and an index.js for the routes.

Type definitions

As you can see here, we have 3 types and 3 models in our database. Let’s create another folder for the database models:

Models

In the orders, we create our order model:

const mongoose = require("mongoose");

const orderSchema = new mongoose.Schema({
userId: {
type: String,
required: true,
},
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
required: true,
},
address: {
type: String,
required: true,
},
city: {
type: String,
required: true,
},
country: {
type: String,
required: true,
},
zipCode: {
type: String,
required: true,
},
totalAmount: {
type: Number,
required: true,
},
items: {
type: String,
required: true,
},
createdDate: {
type: String,
required: true,
},
});

const Order = mongoose.model("Order", orderSchema);

exports.Order = Order;

Create the product model:

const mongoose = require("mongoose");

const productSchema = new mongoose.Schema({
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
},
discountPercentage: {
type: Number,
required: true,
},
rating: {
type: Number,
required: true,
},
stock: {
type: Number,
required: true,
},
brand: {
type: String,
required: true,
},
category: {
type: String,
required: true,
},
thumbnail: {
type: String,
required: true,
},
images: {
type: String,
required: true,
},
});

const Product = mongoose.model("Product", productSchema);

exports.Product = Product;

And finally our user model:

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
minlength: 5,
maxlength: 50,
},
email: {
type: String,
required: true,
unique: true,
minlength: 5,
maxlength: 225,
},
password: {
type: String,
required: true,
unique: true,
minlength: 5,
maxlength: 1024,
},
isAdmin: {
type: Boolean,
default: false,
},
});

const User = mongoose.model("User", userSchema);

exports.User = User;

These models are MongoDB models that we’ll be using to save and retrieve information from the database. In the context of a Node.js application, MongoDB models represent the structure of the data you want to store in your database. They define the schema and behavior of the data, making it easier to interact with the MongoDB database. By defining models, you can enforce a specific structure for your data and perform operations like inserts, updates, and queries efficiently. This not only simplifies the interaction with the database but also enhances code organization and maintainability, making MongoDB models a fundamental part of your application’s data layer.

Now we have the models we can create the OrderType:

const graphql = require("graphql");
const {
GraphQLObjectType,
GraphQLInt,
GraphQLString,
GraphQLFloat,
GraphQLList,
} = graphql;

const OrderType = new GraphQLObjectType({
name: "Order",
fields: () => ({
id: { type: GraphQLString },
userId: { type: GraphQLString },
firstName: { type: GraphQLString },
lastName: { type: GraphQLString },
address: { type: GraphQLString },
city: { type: GraphQLString },
country: { type: GraphQLString },
zipCode: { type: GraphQLString },
totalAmount: { type: GraphQLFloat },
items: { type: GraphQLString },
createdDate: { type: GraphQLString },
}),
});

module.exports = OrderType;

In GraphQL, a type definition like the one you provided represents the structure of data that can be queried or manipulated. Let me explain it in simpler terms:

Imagine you have an online store, and you want to retrieve information about orders placed by customers. You’d define an “OrderType” using GraphQL to specify what kind of data you can request about an order.

Here’s a breakdown of the code you provided:

  1. You import the necessary modules from the “graphql” package.
  2. You define the “OrderType” using the “GraphQLObjectType” constructor. This is where you describe the structure of an order.
  • “name” is a label for this type, in this case, “Order”.
  • “fields” is a function that returns an object. This object describes the properties (fields) of an order, and each field is associated with a specific data type.

3. Inside the “fields” object, you define various properties that an order can have:

  • “id,” “userId,” “firstName,” “lastName,” and so on are fields that describe different aspects of an order.
  • “type” specifies the data type for each field. For example, “id” and “userId” are of type “GraphQLString,” which means they hold text data.
  • “totalAmount” is of type “GraphQLFloat,” indicating it holds numeric data.
  • “items” is also of type “GraphQLString,” but this might represent a serialized list of items, like a JSON string.

4. Finally, you export the “OrderType” so that you can use it in your GraphQL schema to define the structure of your data.

In summary, the “OrderType” is a blueprint that defines the structure of an order in your online store’s data. When a GraphQL query is made, it specifies which fields (properties) of an order should be included in the response. This makes it very flexible for clients to request only the data they need, and the “OrderType” helps ensure that the data conforms to a specific structure.

We do the same for the product:

const graphql = require("graphql");
const { GraphQLObjectType, GraphQLInt, GraphQLString, GraphQLFloat } = graphql;

const ProductType = new GraphQLObjectType({
name: "Product",
fields: () => ({
id: { type: GraphQLString },
brand: { type: GraphQLString },
category: { type: GraphQLString },
description: { type: GraphQLString },
discountPercentage: { type: GraphQLFloat },
images: { type: GraphQLString },
price: { type: GraphQLFloat },
rating: { type: GraphQLFloat },
stock: { type: GraphQLInt },
thumbnail: { type: GraphQLString },
title: { type: GraphQLString },
}),
});

module.exports = ProductType;

And also for the users:

const graphql = require("graphql");
const { GraphQLObjectType, GraphQLInt, GraphQLString, GraphQLFloat } = graphql;

const UserType = new GraphQLObjectType({
name: "User",
fields: () => ({
id: { type: GraphQLString },
username: { type: GraphQLString },
email: { type: GraphQLString },
password: { type: GraphQLString },
isAdmin: { type: graphql.GraphQLBoolean },
}),
});

module.exports = UserType;

Once we have defined our GraphQL types, we can proceed to define our queries and mutations. GraphQL queries serve the purpose of retrieving data, enabling clients to request precisely the information they require. Conversely, mutations are employed to alter server data, providing a reliable and secure means of executing write operations.

Let’s write the code into index.js:

// Import necessary GraphQL modules and dependencies.
const graphql = require("graphql");
const {
GraphQLSchema,
GraphQLObjectType,
GraphQLInt,
GraphQLString,
GraphQLFloat,
GraphQLList,
} = graphql;

// Import data models for products and orders.
const { Product } = require("../models/products");
const { Order } = require("../models/orders");

// Import user-defined data types for GraphQL.
const ProductType = require("./TypeDefs/ProductType");
const UserType = require("./TypeDefs/UserType");
const OrderType = require("./TypeDefs/OrderType");

// Define the RootQuery, which is the entry point for querying data.
const RootQuery = new GraphQLObjectType({
name: "RootQueryType",
fields: {
// Products Queries
getAllProduct: {
type: new GraphQLList(ProductType), // Define the type of data to be returned (a list of products).
args: { id: { type: GraphQLString } }, // Specify any input arguments that can be used in the query (in this case, an 'id').
async resolve(parent, args) {
// The 'resolve' function specifies how to fetch and return the requested data.
// In this case, it fetches and returns a list of all products.
const productList = await Product.find();
return productList;
},
},
getProduct: {
type: ProductType, // Define the type of data to be returned (a single product).
args: { id: { type: GraphQLString } }, // Specify an input argument 'id'.
async resolve(parent, args) {
// The 'resolve' function fetches and returns a specific product based on the provided 'id'.
const product = await Product.findById(args.id);
return product;
},
},

// Orders Queries
getAllOrders: {
type: new GraphQLList(OrderType), // Define the type of data to be returned (a list of orders).
args: { id: { type: GraphQLString } }, // Specify an input argument 'id'.
async resolve(parent, args, req) {
// The 'resolve' function fetches and returns a list of orders for a specific user, but only if the user is authenticated.
if (!req.isAuth) {
throw new Error("Unauthenticated");
}
const orderList = await Order.find({ userId: args.id });
return orderList;
},
},
},
});

// Export a GraphQLSchema that includes the RootQuery.
module.exports = new GraphQLSchema({
query: RootQuery,
});

Root Queries:

  • The RootQuery is the entry point for querying data. It's like the main menu of options you have when you want to retrieve information.
  • In this code, we have defined several queries for retrieving data, such as getAllProduct, getProduct, and getAllOrders. These queries specify what data we can request.

Fields:

  • Within the RootQuery, each query is defined as a "field." For example, getAllProduct, getProduct, and getAllOrders are fields.
  • These fields specify the types of data they return and any input arguments they accept.

Types:

  • GraphQL is strongly typed. Each query and the data it returns are defined by types. For example, ProductType and OrderType describe the structure of products and orders.

Resolvers:

  • The resolve functions are responsible for fetching and returning the actual data when a query is executed. They determine how to retrieve the requested information from a database or other data source.

Input Arguments:

  • Queries can accept input arguments to customize the results. For example, you can use the id argument to specify which product or order you want to retrieve.

Now we created the schemas and our root queries let’s add our endpoint in the index.js:

const express = require("express");
const app = express();
const mongoose = require("mongoose");
const PORT = 5000;
const { graphqlHTTP } = require("express-graphql");
const schema = require("./Schemas");

mongoose
.connect("mongodb://127.0.0.1/{Your db collection name}")
.then(() => console.log("Connected to MongoDB..."))
.catch((err) => console.error("Could not connect to MongoDB..."));

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

app.listen(PORT, () => {
console.log("Server running");
});

Having defined our queries, the next step is to interact with our database by adding, updating, or deleting data. In GraphQL, this task is addressed using mutations. Mutations provide a mechanism to modify the data within the server. They allow clients to create, edit, or remove records while ensuring a consistent and controlled way to perform write operations.

Let’s back to index.js where we defined the queries and add some mutations:

...
const Mutation = new GraphQLObjectType({
name: "Mutation",
fields: {
//Product mutations
createProduct: {
type: ProductType,
args: {
brand: { type: GraphQLString },
category: { type: GraphQLString },
description: { type: GraphQLString },
discountPercentage: { type: GraphQLFloat },
images: { type: GraphQLString },
price: { type: GraphQLFloat },
rating: { type: GraphQLFloat },
stock: { type: GraphQLInt },
thumbnail: { type: GraphQLString },
title: { type: GraphQLString },
},
async resolve(parent, args, req) {
const newProduct = new Product({
title: args.title,
brand: args.brand,
category: args.category,
description: args.description,
discountPercentage: args.discountPercentage,
images: args.images,
price: args.price,
rating: args.rating,
stock: args.stock,
thumbnail: args.thumbnail,
});

await newProduct.save();

return newProduct;;
},
},

updateProduct: {
type: ProductType,
args: {
id: { type: GraphQLString },
brand: { type: GraphQLString },
category: { type: GraphQLString },
description: { type: GraphQLString },
discountPercentage: { type: GraphQLFloat },
images: { type: GraphQLString },
price: { type: GraphQLFloat },
rating: { type: GraphQLFloat },
stock: { type: GraphQLInt },
thumbnail: { type: GraphQLString },
title: { type: GraphQLString },
},
async resolve(parent, args, req) {
const newProduct = await Product.findByIdAndUpdate(args.id, {
brand: args.brand,
category: args.category,
description: args.description,
discountPercentage: args.discountPercentage,
images: args.images,
price: args.price,
rating: args.rating,
stock: args.stock,
thumbnail: args.thumbnail,
title: args.title,
});

return newProduct;
},
},

deleteProduct: {
type: ProductType,
args: {
id: { type: GraphQLString },
},
async resolve(parent, args) {
console.log(args.id);

const deletedProduct = await Product.findByIdAndDelete(args.id);

return args;
},
},

}});

module.exports = new GraphQLSchema({
query: RootQuery,
mutation: Mutation, //add mutations here
});
  1. Create Product Mutation:
  • The createProduct mutation allows you to add a new product to the database.
  • It accepts various product details as arguments, such as brand, category, description, and price.
  • It creates a new product using the provided arguments and saves it to the database.
  • Finally, it returns the details of the newly created product.

2. Update Product Mutation:

  • The updateProduct mutation is used to modify an existing product in the database.
  • It takes the product’s ID along with various details (e.g., brand, category, and price) as arguments.
  • It finds the existing product by its ID and updates its details with the provided arguments.
  • The updated product details are then returned.

3. Delete Product Mutation:

  • The deleteProduct mutation is designed to remove a product from the database.
  • It requires the product’s ID as an argument.
  • It deletes the product with the specified ID from the database.
  • The mutation returns the ID of the deleted product.

Now that we have everything set up for interacting with GraphQL, it’s fortunate that GraphQL provides an excellent interface called GraphiQL. By visiting the http://localhost:5000/graphql endpoint, you can easily explore and interact with your data. Here are some cool features of GraphiQL.

You can learn more from here: https://www.gatsbyjs.com/docs/how-to/querying-data/running-queries-with-graphiql/

GraphiQL Features:

  1. Interactive Interface: GraphiQL provides a web-based, interactive environment where you can compose and send GraphQL queries and mutations.
  2. Auto-Complete and Documentation: It offers auto-complete suggestions for query and mutation fields and displays documentation about your GraphQL schema, making it easy to explore the available data and operations.
  3. History and Presets: You can save and reuse previous queries and share them with your team. It’s a handy feature for iterative development.
  4. Real-Time Validation: GraphiQL provides instant feedback on query and mutation syntax errors, helping you avoid common mistakes.
  5. Responsive and Customizable: The interface is responsive, works well on various screen sizes, and can be customized to suit your needs.

Let’s first add some data into our database:

  1. Creating a New Product:
mutation {
createProduct(
title: "First Product",
brand: "Example Brand",
category: "Electronics",
description: "A new product description.",
discountPercentage: 5,
images: "image path",
price: 299.99,
rating: 4.7,
stock: 5,
thumbnail: "thumbnail path",
) {
id
title
}
}

This mutation adds a new product with the specified details and returns the id and title of the newly created product.

2. Updating and existing product

mutation {
updateProduct(id:"653fbae451de02c94aef4382", description: "Update description for the first product"){
id,
description,
title
}
}

This mutation modifies the description of the product with the given id and returns the id, title and the updated description.

3. Deleting a product

mutation{
deleteProduct(id:"653fbae451de02c94aef4382"){
id
}
}

Now that we have successfully used our mutations, it’s time to try out our queries. First, I have added three new products to the database.

Query to get all products from database:

query{
getAllProduct{
id,
title,
brand
}
}

Here is one of the great advantages of GraphQL: you can choose precisely what data you want to retrieve from the database. In this case, I am requesting the product’s ID, title, and brand to be returned.

Query to get a specific product by ID:

query {
getProduct(id: "653fcc789b3cf5f5cfa3e48d") {
id
title
brand
category
description
price
stock
}
}

Here also you can choose what data you want to retrieve from the database.

Now we’ve gained some insight into using GraphQL with Node.js. However, this is only the beginning. As you can see, the significant advantage lies in using just one URL for all operations, and the ability to dynamically determine the data we want from the database.

I hope this served as a good introduction to GraphQL. In the next part, I will introduce the authentication method with GraphQL and create the order mutation. You can find the full code on GitHub.

See you in the next part!

--

--

Tókos Bence

Hi everyone! I'm an enthusiastic full-stack developer. Please feel free to reach out to me via email (tokosbex@gmail.com) or Twitter (@tokosbex).