DEV Community

Cover image for Nesting GraphQL with MongoDB
Jenny Yang
Jenny Yang

Posted on

Nesting GraphQL with MongoDB

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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 you are going to use these schemas

Logic 1. Defining MongoDB Schema

We are making Transaction schema looking like this:

Transaction {
  price
  method
  cardNumber
  paidTime
  items: [
    {
      amount
      quantity  
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Break down

  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.js

import { 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

`;
Enter fullscreen mode Exit fullscreen mode

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

Break down

  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
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's create resolver file for function and you will need to pass it to the 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;
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

1_HAV3inx1W2kKT2A9WBKWfg

Firstly, createTransaction is an async function, takes a few arguments but we 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');
Enter fullscreen mode Exit fullscreen mode
Create transaction

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

1_JmocGsZYL3icZVf047D61Q

mutation {
   functionName(input arguments) { 
     data you want to return, you can be selective
   }
}
Enter fullscreen mode Exit fullscreen mode
Create Items

You can push items into a transaction that you selected with id.

1_tfTyl7J56SqE6QwmmX9LHw

Remember, the transactionId should exist in your database.

Fetching query

// typeDefs.js
import { 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]
  }
`;
Enter fullscreen mode Exit fullscreen mode

Type definition of Item Shcema

Type definition of Transaction Schema (notice Item type definition is nested in Transaction type definition in items field)
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 ... }
}
Enter fullscreen mode Exit fullscreen mode

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 type
type Query {
    transactions: [Transaction!]!
  }
Enter fullscreen mode Exit fullscreen mode

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 the object as the image below.

query {
  transactions {
     id
     method
     cardNumber
     PadTime
     items {
       id
       amount
       quantity
     }
   }
}
Enter fullscreen mode Exit fullscreen mode

1_1llSDAjC-arKXMl3PXRALQ

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.

Top comments (0)