DEV Community

loading...

5 hours GraphQL Tutorial with NodeJS and Express

Tran Minh Tri
FullStack Developer at SendFX
・8 min read

This is a tutorial that will show you how to make a graphQL server using graphQL, NodeJS, Express, MongoDB.

At the end of this tutorial. You should know how graphQL works, be able to set-up a basic server that has one endpoint but allow you to gather information from many different databases in a single cluster.

Why GraphQL?

Normally, when you build APIs, you normally have many different endpoints for many different purposes.

The result is when you need changes on the front-end, you have to go back to the back-end and fix the API to make it return the right data. It is easy for you to do this when you have 10 APIs, but when you have 40, 50+ APIs with different complicated data to return it makes things harder to maintain.

how grapql works

This same problem happens to Facebook so they decide to invent graphQL. Now we have many more pros like :

  • Smaller payload
  • Fetching all data as one
  • Pre-defined Schema ( How your data should look like, this helps you have better understanding of data-flow in your app )

We also have some cons as well :

  • Learning Curve (Yes! You need to spend time learning it )
  • Not popular ( It is strong. But GraphQL just provides a new way to manage and write your APIs. It does feel life-changing but RESTful APIs work just fine in many cases. In fact, you may never need GraphQL if you don't do it in your day-to-day job as a developer ).

Still, If you still like GraphQL and want to see how you can create a basic server that allows you to fetch data from graphQL. Then this tutorial is for you.

Tutorial :

Installing npm packages :

_ Firstly, you need to choose a directory and run npm init to start the project.
_ Then install these packages (we will need them later on ):

  • cors ( workaround CORS problems )
  • dotenv ( using environment variables )
  • express ( express server )
  • express-graphql ( connecting express with graphql )
  • mongoose ( working with mongoDB )
  • nodemon ( keep your server on watch mode )

Your packages.json should look like this (versions of packages are not important, you just need to install the latest) :
packages

SetUp a databases with MongoDB

We also need free-tier databases hosted on MongoD so we can see if our API works.

You should be able to go to MongoDB create an account if you don't have one and create a project.

MongoDB

When you have the project, choose connect > connect using mongodb compass and try to look for the string like this :

mongodb+srv://admin:${process.env.MONGO_DB_PASSWORD}@graphql.oumb8.mongodb.net/test

The admin and password could be different but it is okay. That's on you. Now just save it somewhere we will need it later.

SetUp Server ( app.js )

const express = require('express');
// grapqlHTTP allows us to use built-in interfaces to test our API
const { graphqlHTTP } = require('express-graphql');

// Using environment variable to store mongoDB databases password
const dotenv = require('dotenv');
dotenv.config();

// Connecting mongoose with databases
const mongoose = require('mongoose');
mongoose.connect(`mongodb+srv://admin:${process.env.MONGO_DB_PASSWORD}@graphql.oumb8.mongodb.net/test`,
{ 
    useNewUrlParser: true,
    useUnifiedTopology: true 
});
mongoose.connection.once('open', () => {
    console.log('Connected to databases');
});

// CORS 
const cors = require('cors');

// GrapQL Schema ( We haven't created it yet but we will soon )
const schema = require('./schema/schema');

const app = express();

app.use(cors());
app.use('/graphql', graphqlHTTP({
    graphiql: true,
    schema: schema
}));
const port = process.env.PORT || 5000;

app.listen(port, () => {
    console.log(`SERVER IS RUNNING ON PORT ${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Now I know this is long. But all of the things above are just boilerplate. So don't stress yourself too much. It always required a lot to setup a server even it is just a basic one.

Also, create .env file like this to use your environment variables:

MONGO_DB_PASSWORD=your-mongodb-password
Enter fullscreen mode Exit fullscreen mode

Create Mongoose Model

Mongoose models are used to define how your data will be stored in mongoDB and also it is a way to connect to mongoDB to perform CRUD operations and more.

We will create 3 simple Mongoose Models : User, Hobby, Post
User.js

const mongoose = require('mongoose');
const UserSchema = mongoose.Schema({
    name: String,
    age: Number,
    job: String 
});
module.exports = mongoose.model('User', UserSchema);
Enter fullscreen mode Exit fullscreen mode

Basically, we just create a UserSchema that will have name, age, job fields. Now we just need to do the same with Post and Hobby.

PostModel should have: comment, userId
HobbyModel should have: title, description, userId

I believe you can do it on your own. It is the same as UserModel.

Create GraphQL RootQuery and Mutation:

1.Definition about RootQuery and Mutation :

RootQuery is where you can define what queries you want to build. Basically, all queries of your graphql API are stored in this RootQuery. So you can access it through a single endpoint.

Mutation also just works like RootQuery but now it is used to store methods used to change your data instead of just read it.

Eventually, you will provide it to GraphQLSchema like this :

const RootQuery = ...
const Mutation = ...

module.exports = new graphql.GraphQLSchema({
    query: RootQuery,
    mutation: Mutation
});
Enter fullscreen mode Exit fullscreen mode

2.Define types for RootQuery :

Just like we talked about one of the pros of RootQuery, we have pre-defined schema in GraphQL to help us to know what Queries from RootQuery will return. All of this is thank to types we will define in RootQuery.

A type in GraphQL will have 2 basic fields: name, fields and in every field of fields we have 3 basic fields: type, args (optional), resolve.

Enough for talking, we will now create a UserType to see what is a type in GraphQL.

Schema.js

const graphql = require('graphql');
const User = require('../models/User');
const Post = require('../models/Post');
const Hobby = require('../models/Hobby');

const UserType = new graphql.GraphQLObjectType({
name: 'UserType', //We will use this `name` in RootQuery
fields: () => ({
        id: {type: graphql.GraphQLID},
        name: {type: graphql.GraphQLString},
        age: {type: graphql.GraphQLInt},
        job: {type: graphql.GraphQLString},
        posts: {
            type: graphql.GraphQLList(PostType),
            resolve(parent, args) {
                const postArrays = Post.find({
                    userId: parent.id
                });

                return postArrays;
            }
        },
        hobbies: {
            type: graphql.GraphQLList(HobbyTypes),
            resolve(parent, args) {
                const hobbiesArray = Hobby.find({
                    userId: parent.id 
                });

                return hobbiesArray;
            }
        }
})
});
Enter fullscreen mode Exit fullscreen mode

I will explain. Here in this UserType when we query for users we will expect to see 6 fields in return for each user and that are: id, name, job, age, posts, hobbies.

In this case, posts and hobbies are more special mainly because they have their own databases on MongoDB. we will need to use args (optional) and resolve(parent,args){} to fetch them from their own databases.

args: is where you will provide arguments that will be used in resolve method used to fetch data.

resolve(parent, args){}: is where you fetch your data and will have access to the parent element (User databases) and args that you provided earlier.

In resolve(parent, args){}, we just connect to our databases and doing Mongoose stuffs. This is not a tutorial about how to work with Mongoose so I guess you can figure this out.

Knowing that fields in GraphQL and Mongoose Models don't have to be exactly 100% like each other. Fields in GraphQL act like bridge between your databases where you can keep gathering information by jumping between Types while Model in Mongoose defines how your data will be stored in your databases. These two are different.

You can do the same for HobbyType and PostType then come back here to check.

const HobbyTypes = new graphql.GraphQLObjectType({
    name: 'HobbyType',
    description: "Model for HobbyType",
    fields:() => ({
        id: {type: graphql.GraphQLID},
        title: {type: graphql.GraphQLString},
        description: {type: graphql.GraphQLString},
        user: {
            type: UserType,
            resolve(parent, args) {
                const user = User.findById(parent.userId);

                return user;
            }
        }
    })
});

const PostType = new graphql.GraphQLObjectType({
    name: "PostType",
    description: "Model for PostType",
    fields: () => ({
        id: {type: graphql.GraphQLID},
        comment: {type: graphql.GraphQLString},
        user: {
            type: UserType,

            resolve(parent, args) {
                const user = User.findById(parent.userId);

                return user;
            }
        }
    })
});
Enter fullscreen mode Exit fullscreen mode

In this situation, user field from fields acts like a bridge. You can gather userInfo then in userInfo you will have a post inside a postArray that contain info about the user and this user (same user) have a postArray and the cycle repeat. This is the power of GraphQL. You can freely fetch data by jumping between databases as long as you define them in fields.

3.Building RootQuery :

If you know how to build Types like above, you will know how to build RootQuery. It's basically the same.

const RootQuery = new graphql.GraphQLObjectType({
    name: "RootQuery",
    fields: {
        user: {
            type: UserType,
            args: {
                id: {type: graphql.GraphQLID}
            },
            async resolve(parent, args) {
                const user = await User.findById(args.id);

                return user;
            }
        },
        users: {
            type: graphql.GraphQLList(UserType),
            async resolve(parent, args) {
                const users = await User.find();

                return users;
            }
        },

        hobby: {
            type: HobbyTypes,
            args: {
                id: {type: graphql.GraphQLID}
            },
            async resolve(parent, args) {
                const hobby = await Hobby.findById(args.id);

                return hobby;
            }
        },
        hobbies: {
            type: graphql.GraphQLList(HobbyTypes),
            async resolve(parent, args) {
                const hobbies = await Hobby.find();

                return hobbies;
            }
        },

        post: {
            type: PostType,
            args: {
                id: {type: graphql.GraphQLID}
            },
            async resolve(parent, args) {
                const post = await Post.findById({
                    id: args.id
                });

                return post;
            }
        },
        posts: {
            type: graphql.GraphQLList(PostType),
            async resolve(parent, args) {
                const posts = await Post.find();

                return posts;
            }
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

You will have 6 queries, but if you can understand how we build user query, you will understand the rest.

user query will fetch a user using id provided in args. user has the UserType so when it returns a user, it will return 6 fields including posts and hobbies of this user. posts and hobbies are generated in UserType itself and not by we define posts and hobbies in RootQuery. These two are also different.

That is it for RootQuery. By this time, if you have data stored in MongoDB you will be able to test your GraphQL API using localhost, on how to actually call your GraphQL API I recommend you watch 15 mins tutorial since it really time-consuming for me to explain it here.

4.Building Mutation:

The same idea with RootQuery. I will only post one method for mutation so you can understand how can you write a Mutation for GraphQLSchema :

const Mutation = new graphql.GraphQLObjectType({
    name: 'Mutation',
    fields: () => ({
        createUser: {
            type: UserType,
            args: {
                name: {type:graphql.GraphQLNonNull(graphql.GraphQLString)},
                age: {type: graphql.GraphQLNonNull(graphql.GraphQLInt)},
                job: {type: graphql.GraphQLString}
            },
            async resolve(parent, args) {
                let user = new User({
                    name: args.name,
                    age: args.age,
                    job: args.job
                });
                const newUser = await user.save();
                return newUser;
            }
 })
})
Enter fullscreen mode Exit fullscreen mode

You pass data you need to create a user to args. Then you create a new user in resolve and update it using Mongoose. As simple as that you can create a user through interfaces.

Now you can create more methods like : fetch singleUser, updateUser, deleteUser, createHobby, createPost,... Anything really. After this you can check my github code link below to see if you are right or wrong.

Github Code Base

At this point you should be able to :

  • Create a Type and define it in RootQuery
  • Create methods in Mutation to change your data in your databases
  • Fetching and Mutating all the fields in databases successfully through your local-host.

Alt Text

  • Be able to fetch infinity fields by jumping between two types in graphql like this : Alt Text

From what you learn from this tutorial. You can start building a server of any application you want as long as you know how your data will look like. If you don't like to build anything at least you can understand how GraphQL works in certain ways.

--peace--

Discussion (0)

Forem Open with the Forem app