Background and Introduction

GraphQL is a query language for application programming interfaces (APIs) that is made to give API clients only the data they have specifically queried for. This essentially means you have a single endpoint for the API.

It should be seen as an alternative to REST.

GraphQL Terms

  1. Schema:
    The structure of the different data types that the API will work with.

  2. Query:
    A root type to query the API.

  3. Mutation:
    A root type to post to the API.

  4. Type:
    A defined object to build to the Schema.

  5. Resolver
    A function to implement the query/mutation.

Take Aways

By the end of this tutorial, the following concepts will be covered:

  1. GraphQL Project Set Up
  2. GraphQL Schemas, Queries and Mutations.
  3. Node and mongodb connection

Prerequisites

  1. Knowledge of JavaScript

Environment Set Up

These are the 2 required tools:

  • Node
  • Mongo database.

Setup your environment as follows:

  1. Follow this link to find the relevant installation command for node LTS version.

# As root
curl -sL https://rpm.nodesource.com/setup_lts.x | bash -

# No root privileges 
curl -sL https://rpm.nodesource.com/setup_lts.x | sudo bash -

Verify your installation using:

    node --version
  1. Download and install Mongodb community edition by following this docs

  2. Create a mongodb user and enable access control as shown in this docs

Step 1.Create a node project

In a directory of your choice create a folder that will serve as your project root folder.

In the terminal, go to your new project root folder and run the following command:

    npm init

P.S. you can leave the test field empty 😀

This command will create a package.json file that will define the project dependencies.

Step 2. Define the Project Folder Structure

Define the project folder structure as shown below:


.
├── package.json
└── src
    ├── api
    │   ├── index.mjs
    │   └── shoe.module
    │       ├── shoe.controller.mjs
    │       ├── shoe.model.mjs
    │       ├── shoe.resolver.mjs
    │       └── shoe.schema.mjs
    ├── app.mjs
    └── config
        └── db.mjs

The index.mjs in the api folder will be used to aggregate the type definitions and the resolvers for our GraphQL schema.

This aggregation is critical because we expect the API to have multiple modules.

For this tutorial however we will stick to the single shoe module.

The app.mjs file will hold code to start the server.

The db.mjs in the config folder will connect our db client to the mongo database.

Step 3. Install the Required Dependencies

In the root folder of your project, using npm, install the following dependencies:

  • apollo-server-express
  • express
  • mongoose
    npm install --save apollo-server-express express mongoose

apollo-server-express connects the express and apollo GraphQL servers.
It provides more ability to modify the apollo server and gives you the option of combining both REST and GraphQL technologies in your API.

express is a REST API framework for Node.

mongoose will be used to connect to the Mongo database.

Step 4. Set up app.mjs

The typeDefs and resolvers are imported from the /api/index.mjs file where they are aggregated.

The dbConnect() function which connects the mongoose to mongodb is also run here.

The express app is linked to the apollo server using the applyMiddleware function as shown in the code.

The app is configured to listen to a port number stored in the environment variable or one that is manually set.

In the app.mjs file add the following lines of code:


import { createRequire } from 'module';
const require = createRequire(import.meta.url);

const { ApolloServer } = require('apollo-server-express');
const express = require('express');

import { typeDefs } from './api/index.mjs';
import { resolvers } from './api/index.mjs';

import { dbConnect } from './config/db.mjs';


dbConnect();

const app = express();
const PORT = 9200;
const PATH = '/graphql'
const server = new ApolloServer({ typeDefs, resolvers });

server.applyMiddleware({ app, path: PATH });

app.listen({ port: process.env.PORT || PORT }, () =>
    console.log(`🚀 Server ready`)
);

Step 5. Connect to the database

Create and export the dbConncect() function that is imported in app.mjs as shown in the code in step 4.
In the config/db.mjs file connect to the database as shown below:




import mongoose from 'mongoose';


mongoose.PromiseProvider = global.Promise;

export const dbConnect = () => mongoose.connect('mongodb://<user>:<password@<db domain or ip address>:27017/<collection-name>?authSource=admin', { useNewUrlParser: true });


If your database is hosted remotely, remember to configure it to allow remote access.
This article will be of help.

Step 6. Set up the shoe models

The Schema object imported from mongoose is used to define a mongodb schema.
The defined schema is exported as Shoe and will be used to CRUD the mongo database.

For this case I used the shopEmail field to store the shop associated with a shoe, votePlus and voteMinus fields to store a tally of the number of people who liked or disliked the shoe and the url field to store the location of the photo.

Define the model schema in the shoe.module/shoe.models.mjs file as shown below:


import mongoose from 'mongoose';


const { Schema } = mongoose;


const shoeSchema = new Schema({

    shopEmail: {
        type: String,
        required: [true, 'Email is required'],
    },

    votePlus: {
        type: Number,
    },

    voteMinus: {
        type: Number,
    },

    url: {
        type: String,
        required: [true, 'Email is required'],
        unique: [true, 'Email is unique']
    },

});


export default mongoose.model('Shoe', shoeSchema);

Step 7. Create the GraphQL Schema.

GraphQL demands that you design a schema which will define the API of your server.

GraphQL has a schema definition language that helps in defining the schema.

A complete schema is a collection of root types such as Mutation, Query and User defined types such as object, enum, interface and union types.

use the imported gpl to define the schema as shown in the code within this step.

Types we have used

  1. Object Type:
    The type Shoe is an object type with fields defined as non-null Strings and non-null Integers.

Types ShoeMutationNotCommitted, ShoeMutationCommitted, ShoeQueryNotFound, ShoeQueryFound are object types that define different fields based on the failure or success of mutation and query requests.

  1. Query Type:
    This is a root type used to Read/Query from the API.
    In REST, this would be a GET method.

The Query type is defined with the keyword extended to enable aggregation of multiple Query types from multiple modules.

The Query type defines 2 resolvers getAllShoes and getShopShoes.
Resolvers are not types but are functions to perform Queries and mutations based on the defined types.

  1. Mutation Type
    This is a root type used to write to the API.
    In REST, this would be a POST/PUT method.

The Mutation defines the createShoe resolver.

  1. Union Types

A Union Type indicates that a field can return one of a multiple object types.
The Union Types hold the possible returned types for the Mutation and Query resolvers. A Mutation/Query could complete or fail to complete. The set Unions expects either a success or fail type.

Essentially for this project, Unions will be used for error handling.

The Schema is exported as shoeTypeDefs for use during aggregation with other existing schemas as shown in step 10.

Define the schema in the shoe.module/shoe.schema.mjs file as shown below:


import { createRequire } from 'module';
const require = createRequire(import.meta.url);

const { gql } = require('apollo-server-express');


export const shoeTypeDefs = gql`

    type Shoe {
      url: String!
      votePlus: Int!
      voteMinus: Int!
      shopEmail: String!
    }
    
    type ShoeMutationNotCommitted {
      code: Int!
      message: String!
    }
    
    type ShoeMutationCommitted {
      code: Int!
      message: String!
      data: Shoe!
    }
    
    union ShoeMutationResult = 
          ShoeMutationCommitted | ShoeMutationNotCommitted
    
    extend type Mutation {
      createShoe(url: String!, shopEmail: String!): ShoeMutationResult!
    }
    
    
    type ShoeQueryNotFound {
      code: Int!
      message: String!
    }
    
    
    type ShoeQueryFound {
      code: Int!
      message: String!
      data: [Shoe]!   
    }
    
    
    union ShoeQueryResult = ShoeQueryFound | ShoeQueryNotFound
    
    
    extend type Query {
   
      getAllShoes: ShoeQueryResult!
      getShopShoes(shopEmail: String!): ShoeQueryResult!
    }
`;

Step 8. Create the Resolver functions.

In step 7 above, the resolvers are mentioned as fields in the Query and Mutation types.

They now need to be defined as actual functions.

The names and variables in these functions must match the ones defined in the schema in step 7 above.

The Shoe is imported from the model created in step 6 above.
Shoe will be used to perform CRUD Operations on the mongo database.

The __typname field is what schema Union types will use to decide what schema object result type the resolver returned.

This Union type concept is used to return different codes and messages to GraphQL clients based on the success or failure of mutations and queries.


import Shoe from './shoe.model.mjs';


export async function createShoe(root, req) {

try {

    const value = await Shoe.create({ shopEmail: req.shopEmail, url:                req.url })

    return {
        data: value._doc,
        code: 200,
        __typename: "ShoeMutationCommitted",
        message: "Shoe Created Successfully"
    }

} catch (err) {
    return {
        __typename: "ShoeMutationNotCommitted",
        code: 401,
        message: err.errmsg
    }
}

    

}



export async function getAllShoes(root, req) {

    try {

        const shoe = await Shoe.find({});

        if (shoe) {

            return { 
                data: shoe, 
                code: 200, 
                __typename: "ShoeQueryFound", 
                message: "Shoes Found" 
            }

        }

        return {
            __typename: "ShoeQueryNotFound",
            code: 401,
            message: "Shoes not Found"
        }

        

    } catch (err) {

        console.log(err);

        return {
            __typename: "ShoeQueryNotFound",
            code: 401,
            message: err.errmsg
        }

    }

}


export async function getShopShoes(root, req) {

    try {

        const shoe = await Shoe.find({shopEmail: req.shopEmail});

        if (shoe) {

            return { 
                data: shoe, 
                code: 200, 
                __typename: "ShoeQueryFound", 
                message: "Shoes Found" 
            }

        }

        return {
            __typename: "ShoeQueryNotFound",
            code: 401,
            message: "Shoes not Found"
        }

        

    } catch (err) {

        console.log(err);

        return {
            __typename: "ShoeQueryNotFound",
            code: 401,
            message: err.errmsg
        }

    }

}

Step 9. Register the Resolvers.

Import the exported resolver functions created in step 8 above and register them as resolvers in the shoe.module/shoe.resolvers.mjs file as shown in the code below.

The resolvers are exported as shoeResolvers for aggregation purposes.


import {
    createShoe,
    getShopShoes,
    getAllShoes,
} from './shoe.controller.mjs';


export var shoeResolvers = {
    Mutation: {
        createShoe,
    },
    Query: {
        getShopShoes,
        getAllShoes,

    }
};

Step 10. Aggregate the Schemas and Resolvers

The GraphQL Apollo can only accept a single schema and a single resolver object.

In a case where the API is broken down i.e we have multiple schema and resolvers definitions, we must combine them into single definitions.

There are multiple ways of achieving this aggregation.

For this case:
In the api/index.mjs file,create a Root schema with types Query and Mutation that will be extended by the other schema files.

Combine the Root schema with the different imported schemas using an Array.

Combine the different resolvers using an Array as shown below:


import { createRequire } from 'module';
const require = createRequire(import.meta.url);

const { gql } = require('apollo-server-express');


import { shoeTypeDefs as Shoe } from './ShoeModule/shoe.schema.mjs';
import { shoeResolvers } from './ShoeModule/shoe.resolver.mjs';



const Root = gql `
        type Query {
            root: String
        }
        type Mutation {
            root: String
        }        
`


export const typeDefs =
    [Root, Shoe]



export const resolvers = 
    [shoeResolvers]

More Schemas and Resolvers

If you have more schemas and resolvers, as is expected import them as shown below:


import { shopTypeDefs as Shop } from './ShopModule/shop.schema.mjs';
import { shopResolvers } from './ShopModule/shop.resolver.mjs';

and add them to the aggregation arrays as shown below:


export const typeDefs =
    [Root, Shoe, Shop ]


export const resolvers = 
    [shoeResolvers, shopResolvers]

N/B Some people prefer having 1 huge schema and resolvers file.

Step 11. Start the App

package.json file

The package.json file holds the name of the file that is run when you start the App.

Update it by adding the start field as follows:


{
  "name": "anyungu-graphql",
  "version": "1.0.0",
  "description": "Silhoutte project for node and graphql",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon --experimental-modules src/app.mjs"
  },
  "author": "Anyungu C.",
  "license": "ISC",
  "dependencies": {
     ...
    "apollo-server-express": "^2.17.0",
    "express": "^4.17.1",
    "mongoose": "^5.10.5"
    ...
    
  }
}

nodemon

If you do not have nodemon, install it globally using:

npm install -g nodemon

For the project only, install it in the project's root folder using:

npm install --save nodemon

nodemon is a tool that helps develop node.js based applications by automatically restarting the node application when file changes in the directory are detected.

In production use node and not nodemon.

npm start

Run this command in the root of your project:

npm start

This will run the app.mjs file using nodemon essentially starting the express server.

Step 12. Test the App.

The GrahpQL playground can be accessed through <ip/host>/graphql.

Mine can be seen here

Understanding the GraphQL Playground

Schema and Docs

The schema and the docs can be accessed by expanding the 2 right side buttons.

The docs describe the the different Query and Mutation resolvers, their expected argument variables and their expected returned data.

The section with the # Write your query or mutation here is the Query/Mutation playground.

Understanding GraphQL Syntax

Queries


query getUser($email: email) {
  getUserByEmail(email: $email) {
    name
    email
    age
  }
}

The above gql creates a getUser query and passes an email variable which is passed on to the getUserByEmail resolver.
This query only returns the 3 fields requested by the user regardless of how many more fields exist.


query getUserFriends($email: email) {
  getUserFriends(email: $email) {
    name
    friends {
        name
    }
  }
}

Assuming every user has an associated list of friends we can return specific fields from the list as shown in the Query above.

The query will return data in the format shown below:


{
  "data": {
    "getUserFriends": {
      "name": "anyungu",
      "friends": [
        {
          "name": "myself"
        },
        {
          "name": "me"
        },
        ...
      ]
    }
  }
}

If the query expects a Union type like the ones in our project, the query can be defined as shown below:


query getAllShoes {
  
  getAllShoes {
    ... on ShoeQueryFound {
      code
      message
      data {
        url
        shopEmail
        votePlus
        voteMinus
      }
    }
    
    ... on ShoeQueryNotFound {
        code
        message
    }
  }
  
}

The expected JSON response data could be one of the 2 options shown below:


{
  "data": {
    "getAllShoes": {
      "code": 200,
      "message": "Shoes Found",
      "data": []
    }
  }
}

_____

{
  "data": {
    "getAllShoes": {
      "code": 400,
      "message": "Error Message",
     
    }
  }
}

Mutations


mutation addUserFreind($user: email, $friend: friendObject) {
  createFriend(episode: $ep, review: $review) {
    email
    code
  }
}

The above function passes required variables to the createFriend resolver and returns the email and code fields.

If the mutation expects a Union type result, like in this case, it can be defined as follows:


mutation createShoe {
  
  createShoe(shopEmail: "lol", url: "duka") {
    ... on ShoeMutationCommitted {
      code
      message
      data {
        shopEmail
      }
    }
    
   ... on ShoeMutationNotCommitted {
    code
    message
  }
  }
  
}

The expected response JSON data will be one of the 2 options shown below:


{
  "data": {
    "createShoe": {
      "code": 200,
      "message": "Shoes Found",
      "data": []
    }
  }
}

______

{
  "data": {
    "createShoe": {
      "code": 401,
      "message": "Error Message",
     
    }
  }
}

Errors

GraphQL provides a special errors object when resolvers have syntax and field errors:

A sample error object is shown below:


{
  "errors": [
    {
      "message": "Expected Iterable, but did not find one for field ShopMutationCommitted.data.",
      "locations": [
        {
          "line": 42,
          "column": 7
        }
      ],
      "path": [
        "createShop",
        "data"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "message": "Expected Iterable, but did not find one for field ShopMutationCommitted.data."
        }
      }
    }
  ],
  "data": null
}

Next Steps, Remarks and Conclusion

Next Steps

The GraphQL node API can be deployed as a normal node application using docker or as an application in any cloud service provider.

More information about GraphQL can be found here

Remarks

GraphQL is a great query language for APIs because it converts your APIs from having multiple endpoints to having a single endpoint where clients can access resources by specifying what they want in a single query. Besides, it is a good idea to combine both REST and GraphQL.

This tutorial goes through how to build your GraphQL node API with a mongodb data source. In production, the code should be more optimized and have more security and the GraphQL UI playground should be disabled.

Find more code on Github

You've successfully subscribed to Decoded For Devs
Welcome back! You've successfully signed in.
Great! You've successfully signed up.
Your link has expired
Success! Your account is fully activated, you now have access to all content.