Nesting GraphQL With MongoDB

Jenny Yang
The Startup
Published in
6 min readNov 4, 2020

--

Getting Started

GraphQL, Apollo server and MongoDB all connected on your app.

Dependencies to install

devDependencies are optional, only for the sake of your convenience.

// package.json
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon --exec babel-node src/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"apollo-server-express": "^2.19.0",
"express": "^4.17.1",
"graphql": "^15.4.0",
"mongoose": "^5.10.11"
},
"devDependencies": {
"@babel/cli": "^7.12.1",
"@babel/core": "^7.12.3",
"@babel/node": "^7.12.1",
"@babel/preset-env": "^7.12.1",
"nodemon": "^2.0.6"
}
}

How it works

There are three things to define to use graphQL and the logic might not be specifically applied to MongoDB + graphQL. The logic is simple.

  1. Let MongoDB how your schemas look like
  2. Let GraphQL how your schemas look like
  3. Let Apollo Server how your going to use these schema

Logic 1. Defining MongoDB Schema

We are making Transaction schema looking like this:

Transaction{
price
method
cardNumber
paidTime
items: [
{
amount
quantity
}
]
}

We are going to use mongoose as ORM for MongoDB. You just need to define its data type and any additional options if any. It is also very simple. You just define each schema and put them together. MongoDB Schema would look like this:

import mongoose from 'mongoose';const Schema = mongoose.Schema;const itemSchema = new Schema({
amount: { type: Number },
quantity: { type: Number },
});const transactionSchema = new Schema({
price: { type: Number, required: true },
method: { type: String, default: 'VISA', required: true },
cardNumber: { type: String, required: true },
paidTime: { type: Date, default: new Date(), required: true },
items: [itemSchema],
});
export const Transaction = mongoose.model('Transaction', transactionSchema);
  1. Import mongoose
  2. Create a schema instance for item (itemSchema)
  3. Create a schema instance for transaction (transactionSchema)
  4. put itemSchema into items property of transactionSchema object

Notice itemSchema is going to be part of transactionSchema as an array.

Logic 2. Defining TypeDefs

Let’s create a type definition. We are going to use Apollo Server as middleware to handle graphQL. There are other middlewares such as graphql yoga, but Apollo Server is a standard.

Query does things corresponding to GET request, Mutation takes care of any other requests that cause mutation of data such as POST, PUT and DELETE. We are starting by Mutation, because we will push data first, and then fetch them to check if the data properly saved.

There are four type definitions I used in this tutorial:

  • type SchemaName {types} : Defining schema type
  • input nameOfInput {types} : Defining schema’s input, used to type argument’s type
  • type Query {types}: Defining query structure matching to your resolver
  • type Mutation {types}: Defining mutation structure matching to your resolver
// typeDefs.jsimport { gql } from 'apollo-server-express';export const typeDefs = gql`
scalar Date
// Defining your Query// 1 Defining your graphql schema type
type Item {
id: ID!
amount: Float
quantity: Int
}
type Transaction {
id: ID!
price: Float!
method: String!
cardNumber: String!
paidTime: Date!
items: [Item]
}
// 2 Defining input type input ItemInput { transactionId: String!
amount: Float
quantity: Int
}

input TransactionInput {
price: Float!
method: String!
cardNumber: String!
items: [ItemInput]
}
// 3 Defining your Muation
type Mutation {
createTransaction(TransactionInput: TransactionInput!): Transaction
createItem(ItemInput: ItemInput): Transaction

`;

Note: ! mark means this field is required, notice createItem returns Transaction schema

  1. defined Schema of Item and Transaction
  2. defined type of argument which going to be passed into Mutation function
  3. defined two functions to create transaction and to create item.

Logic 3. Defining how you are going to create or update api

Resolver is a function to handle populating your data into your database, you can define how you are going to fetch data and how you are going to update, create data. Since Apollo-server reads from schema from typeDefs, they will need to match how it is structured.

Basic Resolver Structure

const resolver = {  Query: {
some async function
},
Mutation: {
some async function
}
}

Let’s create resolver file for function and you will need to pass it to apollo server instance. In the Mutation object, add code like so:

Transaction mutation (parent schema)

import { Transaction } from '../models/transaction';export const transactionResolver = {
Mutation: {
createTransaction: async (
_, { TransactionInput: { price, method, cardNumber } }
) => {
const newtransaction = new Transaction({
price,
method,
cardNumber,
});
await transaction.save();
return newtransaction;
},
},
}

Firstly, createTransaction is an async function, takes a few arguments but we will only care about the second argument which is what we are going to push to the database and its type.

Secondly, create a Transaction instance imported from mongoose model with input arguments ( price, method, cardNumber) then save the instance using mongoose.

Finally, return the instance.

Item resolver (child schema)

import { Transaction } from '../models/transaction';export const itemResolver = {
Mutation: {
createItem: async (
-, {ItemInput: {transactionId, amount, quantity} }
) => {
// find the transaction by id
const transaction = await Transaction.findById(transactionId);

// check if the transaction exists
if (transaction) {
// if exists, push datas into items of transaction
transaction.items.unshift({
amount,
quantity
});
} else throw new Error('transaction does not exist');

Create transaction

Now let’s create a transaction! You can open up graphql testing playground at your localserver/graphql

mutation {
functionName(input arguments) {
data you want to return, you can be selective
}
}

Create Items

You can push items in a transaction you selected with id.

Remember, the transactionId should exist in your database.

Fetching query

// typeDefs.jsimport { gql } from 'apollo-server-express';export const typeDefs = gql`
scalar Date
// Defining your Query
type Query {
transactions: [Transaction!]!
}
// Defining your graphql schema type
type Item {
id: ID!
amount: Float
quantity: Int
}

type
Transaction {
id: ID!
price: Float!
method: String!
cardNumber: String!
paidTime: Date!
items: [Item]
}
`;
  1. Type definition of Item Shcema
  2. Type definition of Transaction Schema (notice Item type definition is nested in Transaction type definition in items field)
  3. Create Query type that fetches an array of transaction

Transaction Resolver

import { Transaction } from '../models/transaction';export const transactionResolver = {
Query: {
transactions: async () => {
try {
const transactions = await Transaction.find()
return transactions;
} catch (error) {
throw new Error(error);
}
},
},
Mutation: { mutation code ... }}

Define an async function corresponding to the one in our typeDefs. We defined transactions in Query type in our typeDefs like so:

// typeDef.js - our Query typetype Query {
transactions: [Transaction!]!
}

Now let’s fetch data in our localhost:port/graphql. Easy peasy. Isn’t it? If you are querying data, you can omit query at the beginning of object.

query {
transactions {
id
method
cardNumber
PadTime
items {
id
amount
quantity
}
}
}

Conclusion

Nesting schema is easy, however, we need to be precise how we want it to be. If things are not working check whether your schema field’s names are matching with the one in your resolver and its structure.

--

--

Jenny Yang
The Startup

Self-taught software engineer | Enthusiasm for programming and computer science