GraphQL with Express

GraphQL with Express

The GraphQL Trilogy: Part 2

ยท

12 min read

Welcome to Part 2 of "The GraphQL Trilogy". I'm glad that you decided to follow along. Until now we have understood the basic difference between the RESTful and GraphQL approaches, and also looked at a small example of how a typical graphQL query looks like.

If this is your first encounter with the "GraphQL Trilogy", then you can read part 1 ๐Ÿ‘‰here

In this article we'll be setting up an express server to handle the graphQL api requests for our books website. So let's get started:

Folder Structure

To start with our application let's create a directory first, I'm calling it graphQL-book-app, and add two sub-directories: client and server. The folder structure will look something like this

.
โ”œโ”€โ”€ client
โ””โ”€โ”€ server

The server will contain the backend of our app and client file will contain the frontend. So let's navigate inside our server folder and set up our express server.

Setting up Express Server

Run the below command and it'll setup our package.json file to handle the project dependencies.

npm init -y

Now you need to install all the required dependencies for our app to work. Run the below mentioned command:

yarn add express express-graphql graphql lodash cors

After all the dependencies are installed create an index.js file to start your express and add the below mentioned code

const express = require('express')
const cors = require('cors')

const app = express()
app.use(cors())

app.listen(4000, () =>{
    console.log("server started")
})

You can run this app using the below command:

node index.js

Adding GraphQL Endpoint

As we are aware that unlike REST, graphQL maintains a single supercharged endpoint to retrieve data, so let's proceed by setting up our endpoint.

const {graphqlHTTP} = require('express-graphql')

app.use('/graphql', graphqlHTTP({

}) )

Add the above code to your index.js file.

Here our endpoint is /graphql, and we are using a middleware to handle our graphql requests. The graphqlHTTP function from express-graphql package allows express to understand graphql. Now if you go to http://localhost:4000/graphql, you'll see the error

{"errors":[{"message":"GraphQL middleware options must contain a schema."}]}

To get it working we need a schema. The schema defines the type of data, object types, relation types, and how we can interact with the data.

Setting up a Schema

To set up our schema, create a folder named schema and add a file as schema.js.

const graphql = require('graphql')
const _ = require('lodash')

const { GraphQLObjectType,
    GraphQLID,
    GraphQLString,
    GraphQLSchema,
    GraphQLInt,
    GraphQLList } = graphql

const BookType = new GraphQLObjectType({
    name: 'Book',
    fields: () => ({
        id: { type: GraphQLID },
        name: { type: GraphQLString },
        genre: { type: GraphQLString },
    })
})

const AuthorType = new GraphQLObjectType({
    name: 'Author',
    fields: () => ({
        id: { type: GraphQLID },
        name: { type: GraphQLString },
        age: { type: GraphQLInt },
    })
})

Add the above code to your schema.js file.

Here we have defined two object types : AuthorType (to hold data about author) and BookType (to hold the data about books). The GraphQLObjectType function is generally used to define the new object types for our data. The name field for GraphQLObjectType function is to mark what kind of data this particular object resonates to, the fields field is a callback function which return an object containing all the attributes for our object with their respective types like for String attributes we use GraphQLString, for int attributes we use GraphQLInt and for ids you can use either GraphQLInt or GraphQLString but we chose to use GraphQLID as it allows us to take inputs both as string or numerical values.

Now that we have defined our objects we also need to define what kind of queries we'll be making like for books or authors or books with a particular id.

The RootQuery

In order to define the query types, we'll create another object and call this our RootQueryObject. The below code does this. Add it to schema.js


const RootQuery = new GraphQLObjectType({
    name: 'RootQueryType',
    fields: {
        book: {
            type: BookType,
            args: { id: { type: GraphQLID } },
            resolve(parent, args) {
                //to get data for a particular book from db/other source
            }
        },

        author: {
            type: AuthorType,
            args: { id: { type: GraphQLID } },
            resolve(parent, args) {
                 //to get data for a particular author from db/other source
            }
        },

        authors: {
            type: new GraphQLList(AuthorType),
            resolve(parent, args) {
                // to get all authors
            }
        },

        books: {
            type: new GraphQLList(BookType),
            resolve(parent, args) {
                // to get all authors
            }
        },
    }
})

This RootQuery object contains a name field and a fields object. This fields object contains the query types we'll be making. Like for book, we have type as BookType which tells the type of data it'll retrieve, an args field which tells the argument it can receive (here id of type GraphQLID), and a resolve function which'll return the data. This resolve function takes two arguments: parent and args. This args argument is the same args field defined above. The parent argument is used to refer the parent object (we'll see later).

Now we've got our object types and query type for our schema. The only thing left is to define how the schema is mapped and to define the resolve functions.

Here is the code:


const RootQuery = new GraphQLObjectType({
    name: 'RootQueryType',
    fields: {
        book: {
            type: BookType,
            args: { id: { type: GraphQLID } },
            resolve(parent, args) {
                return _.find(books, { id: args.id })
            }
        },

        author: {
            type: AuthorType,
            args: { id: { type: GraphQLID } },
            resolve(parent, args) {
                return _.find(authors, { id: args.id })
            }
        },

        authors: {
            type: new GraphQLList(AuthorType),
            resolve(parent, args) {
                return authors
            }
        },

        books: {
            type: new GraphQLList(BookType),
            resolve(parent, args) {
                return books;
            }
        },
    }
})

module.exports = new GraphQLSchema({
    query: RootQuery
});

Also I've created a dummy data for our app to work for now

let books = [
    { name: 'Name of the Wind', genre: 'Fantasy', id: '1', authorId: '1' },
    { name: 'The Final Empire', genre: 'Fantasy', id: '2', authorId: '2' },
    { name: 'The Hero of Ages', genre: 'Fantasy', id: '4', authorId: '2' },
    { name: 'The Long Earth', genre: 'Sci-Fi', id: '3', authorId: '3' },
    { name: 'The Colour of Magic', genre: 'Fantasy', id: '5', authorId: '3' },
    { name: 'The Light Fantastic', genre: 'Fantasy', id: '6', authorId: '3' },
];


let authors = [
    { name: 'Patrick Rothfuss', age: 44, id: '1' },
    { name: 'Brandon Sanderson', age: 42, id: '2' },
    { name: 'Terry Pratchett', age: 66, id: '3' }
]

After the schema is created, in the index.js file import the schema and add it to the middleware function.

const schema = require('./schema/schema')

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

The graphiql:true feature allows us to use the GraphiQL tool to manually issue GraphQL queries. If you navigate in a web browser to localhost:4000/graphql, you should see an interface that lets you enter queries. It should look like:

Image 21-11-21 at 2.30 AM.jpg

Get started by writing your first query

{
  books{
    name
    genre
  }
}

To retrieve data you start by adding parenthesis, then the data object you wish to retrieve, here books and after that the data fields you wish to see, here name and genre.

Another example could be like:

{
  book(id:2){
    name
    genre
  }
}

Here we're trying to get the name and genre for a book with id as 2. Some other examples could be like:

{
  authors{
    name
    age
  }
}


{
  author(id:3){
    name
    age
  }
}

But hold on if we try to make a nested query, something like this:

{
  author(id:3){
    name
    age
    books{
      name
    }
  }
}

We end up getting an error. The reason for this being when defining the data object types we haven't defined any relationship between them. Adding a relationship is quite simple:

const BookType = new GraphQLObjectType({
    name: 'Book',
    fields: () => ({
        id: { type: GraphQLID },
        name: { type: GraphQLString },
        genre: { type: GraphQLString },
        author: {
            type: AuthorType,
            resolve(parent, args) {
                return _.find(authors, { id: parent.authorId })
            }
        }
    })
})

const AuthorType = new GraphQLObjectType({
    name: 'Author',
    fields: () => ({
        id: { type: GraphQLID },
        name: { type: GraphQLString },
        age: { type: GraphQLInt },
        books: {
            type: new GraphQLList(BookType),
            resolve(parent, args) {
                return _.filter(books, { authorId: parent.id })
            }
        }
    })
})

The above piece of code does the trick. Now for each BookType we've defined a relation with author object and similarly for AuthorType a relation with books because one author can have many books and one book can have many authors. So now you can easily make nested queries as well.

{
  book(id:2){
    name
    genre
    author{
      name
      books{
        name
      }
    }
  }
}

Here we're retrieving name, genre and author name for book with id 2 and for that particular author w're retrieving all his books. With RESTful approach this would have taken 2 to 3 api calls but here a single query does the job. Writing queries could be troublesome the first time but with practice this becomes fairly easy.

Up until now we've set up our graphQL-express server and we're able to retrieve data as well using graphql queries. Now our next task is to add data.

So let's proceed with next section of our learning...

Mutations in GraphQL

Most discussions of GraphQL focus on data fetching, but any complete data platform needs a way to modify server-side data as well.

In RESTful approach we make GET request to retrieve data while in GraphQL we write queries to retrieve data. Similarly in RESTful approach we make POST request to add data while in GraphQL we use what are called mutations. A mutation in graphql is very much similar to a normal query but it allows us to modify our server-side data.

Before adding the mutations to our code I made some changes the way we're storing our data. Now instead of arrays we're using files to read and write the data. For that create a folder named data and add files books.txt and authors.txt.

Name of the Wind,Fantasy,1,1
The Final Empire,Fantasy,2,2
The Hero of Ages,Fantasy,4,2
The Long Earth,Sci-Fi,3,3
The Colour of Magic,Fantasy,5,3
The Light Fantastic,Fantasy,6,3
Patrick Rothfuss,44,1
Brandon Sanderson,42,2
Terry Pratchett,66,3

Now let's move ahead by adding mutation to our schema. Add the below code to your schema.js file.

const fs = require('fs')
const { getAuthors } = require('../helpers/getAuthors')
const { getBooks } = require('../helpers/getBooks')


let books = getBooks()
let authors = getAuthors()

let authorCount = authors.length
let bookCount = books.length


const addNewAuthor = ({ aname, age }) => {
    let id = authorCount + 1
    let str = `\n${aname},${age},${id}`
    authorCount += 1
    let authorObj = {
        name: aname,
        age,
        id
    }
    authors.push(authorObj)
    fs.appendFileSync('./data/authors.txt', str)

    return authorObj
}

const addNewBook = ({ bname, genre, aid }) => {
    let id = bookCount + 1
    let str = `\n${bname},${genre},${id},${aid}`
    bookCount += 1
    let bookObj = {
        name: bname,
        genre,
        id,
        authorId: aid
    }
    books.push(bookObj)
    fs.appendFileSync('./data/books.txt', str)

    return bookObj
}

const Mutation = new GraphQLObjectType({
    name: 'Mutation',
    fields: {
        addAuthor: {
            type: AuthorType,
            args: {
                name: { type: GraphQLString },
                age: { type: GraphQLInt }
            },
            resolve(parent, args) {
                return addNewAuthor({ aname: args.name, age: args.age })
            }
        },

        addBook: {
            type: BookType,
            args: {
                name: { type: GraphQLString },
                genre: { type: GraphQLString },
                authorId: { type: GraphQLID }
            },
            resolve(parent, args) {
                return addNewBook({ bname: args.name, genre: args.genre, aid: args.authorId })
            }
        }
    }
});

module.exports = new GraphQLSchema({
    query: RootQuery,
    mutation: Mutation
});

We start by creating a mutation object, it has a name attribute and a fields attribute. The fields attribute contains objects which define the type of data we wish to add. Like the addAuthor object has a type field which tells that the data this object will be adding is of type AuthorType, the args attribute defines the arguments passed, and the resolve function adds the information passed to our database/other source. The addNewBook and addNewAuthor methods carry out the task of adding the data passed to our text files. The resolve function returns an object of the data added which can be displayed on the graphiql window.

After the mutation object is created we add it to our schema. And now you can add data as well using graphQL.

The below mentioned code helps to read data from text files :

getAuthors.js

const fs = require('fs')
const getAuthors = () => {
    let authors = []

    let content = fs.readFileSync('./data/authors.txt', { encoding: 'utf8' });
    content.split('\n').forEach(item => {
        let sent = item.split(',')
        authors.push({
            name: sent[0],
            age: sent[1],
            id: sent[2],
        })
    })

    return authors

}

module.exports = {getAuthors}

getBooks.js

const fs = require('fs')
const getBooks = () => {
    let books = []
    let content = fs.readFileSync('./data/books.txt', { encoding: 'utf8' });
    content.split('\n').forEach(item => {
        let sent = item.split(',')
        books.push({
            name: sent[0],
            genre: sent[1],
            id: sent[2],
            authorId: sent[3]
        })
    })

    return books

}
module.exports = {getBooks}

To add data, navigate to http://localhost:4000/graphql, and type:

mutation {
  addAuthor(name:"Frank Herbert", age:55){
    name
  }
}

Now there's a slight difference between the query to retrieve data and the one to add data. You start by adding keyword mutation to specify that this particular query is instead a mutation. Then we specify the field type we wish to add, here addAuthor along with the arguments. Then we specify the attributes we wish to see of the added object like name in this example.

Another example for this could be

mutation {
  addBook(name:"Dune",genre:"Sci-Fi", authorId:4){
    name
  }
}

Here we're adding a new book as specified with mutation keyword and addBook field. And we want to display the name of the returned object. Pretty simple ๐Ÿ™‚.

If you try to retrieve that record using this query

{
  book(id:7){
    name
    author{
      name
    }
  }
}

You'll find the result

{
  "data": {
    "book": {
      "name": "Dune",
      "author": {
        "name": "Frank Herbert"
      }
    }
  }
}

With this we're officially done with setting up our graphql-express server. However there is a small catch for you. For now we can still add records with empty strings for name fields. Now I need you to refer the official graphQL documentation and find a way to prevent our graphql server from accepting null or empty value records.

You can comment out your solution...

Conclusion

In this article we started by setting an express server. Then we added a middleware to add an endpoint for graphql api requests. After that we created our data objects types, defined the RootQuery object to handle the queries. Then we defined our schema. Later we saw some query examples. Then at last we added mutations to our graphql-server to add data.

Read more about graphql with express server ๐Ÿ‘‰ here

Now in the next and the final part of our trilogy we'll be working on the frontend of our application i.e making graphQL api requests via ReactJS web application.

iNos vemos! โœŒ๏ธโœŒ๏ธ

ย