DEV Community

Cover image for Crash course on REST, GraphQL and Graphback #3: GraphQL
Michal Stokluska for AeroGear

Posted on • Edited on

Crash course on REST, GraphQL and Graphback #3: GraphQL


GraphQL


GraphQL is a query language invented by Facebook and is an alternative approach to REST for designing and building APIs. Schema is at the center of any GraphQL server and describes functionality available to clients. Schema has types which define:

  • Relationships between entities - in our case it would be a relationship between users and tasks
  • Data manipulation and operation that can be executed by the client, in our project those will be for example queries to fetch all users or all tasks, and mutations to delete and add a user or a task.

To build our GraphQL server we are going to use the "Schema First" approach, which basically prioritizes building schema in development. It allows me to visualize the data flow between entities and queries/mutations that I might require! We are also going to use Apollo framework for GraphQL server, a library that helps us connect our GraphQL schema to a node server, which is same as express framework for REST.

Requirements

Let's get started

First, think about the schema, what are our entities going to be? What data are we planning to return? What does the client need? Imagine our project, with tasks and users, our GraphQL types will look something like this:

type User {
    id: ID!
    firstName: String!
    lastName: String!
    title: String!
    email: String
}

type Task {
    id: ID!
    title: String!
    description: String!
    status: String!
    assignedTo: [User!]!
}
Enter fullscreen mode Exit fullscreen mode

We are defining two entities, a User and Task entity. Both have different attributes and return types. A client can access a User object or Task object and from there he can access any of the attributes given, however, assignedTo from Task returns a User object. Exclamation mark simply means Required so in our example of assignedTo - the return type is required to be of type an array of Users.

  • In your existing server project, use npm to add the following dependencies:
$ npm install apollo-server-express graphql graphql-import
Enter fullscreen mode Exit fullscreen mode
  • Next, edit our index.js file.
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const schema = require('./schema');

const app = express();

app.use(express.json());

const server = new ApolloServer({ schema });

server.applyMiddleware({
  app,
  path: '/graphql',
});

const port = 4000;

app.listen(port, () => {
  console.log(`🚀 Server is running at: http://localhost:${port}/graphql`);
});
Enter fullscreen mode Exit fullscreen mode

We no longer need the REST methods so you can delete them. Next, we are adding Apollo Server to our project. Then, applying a schema ( that is yet to be implemented ) to our Apollo Server, finally, we can apply middleware to it, which is express and path - also called - endpoint.

  • Create a new folder within our server folder, called schema
  • In schema folder create a file called typeDefs.graphql which is going to hold types that we have specified above. Paste the following code:
type User {
    id: ID!
    firstName: String!
    lastName: String!
    title: String!
    email: String
}

type Task {
    id: ID!
    title: String!
    description: String!
    status: String!
    assignedTo: [User!]!
}
Enter fullscreen mode Exit fullscreen mode
  • Next, we are adding type Query - which enables query support for given queries, for now, let's keep it simple and stick to our basic two queries, tasks, which allows a client to access a list of all tasks, and users, which allows accessing an entire array of users.
type Query {
    tasks: [Task!]!
    users: [User!]!
}
Enter fullscreen mode Exit fullscreen mode
  • Next, add another file called resolvers.js into schema folder and paste the following code:
const { tasks, users } = require('../db');

const resolvers = {
  Query: {
    tasks() {
      return tasks;
    },

    users() {
      return users;
    },
  },
};

module.exports = resolvers;
Enter fullscreen mode Exit fullscreen mode

Resolvers are simply functions resolving value for a type from typeDefs. They can return values like Strings, Numbers, Booleans etc. For example, the users resolver must return an array of Users. They are similar to HTTP handler functions that we saw in express, they implement the business logic and return a result.

  • Create index.js in schema folder and paste following code:
const { importSchema } = require('graphql-import');
const { makeExecutableSchema } = require('graphql-tools');
const resolvers = require('./resolvers');
const typeDefs = importSchema('schema/typeDefs.graphql');

module.exports = makeExecutableSchema({ resolvers, typeDefs });
Enter fullscreen mode Exit fullscreen mode

In this step we have made an executable schema that contains both, our resolvers and typeDefs so it can be used in our index.js

const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const schema = require('./schema'); <-----------------------------

const app = express();

app.use(express.json());

const server = new ApolloServer({ schema });<---------------------

server.applyMiddleware({
  app,
  path: '/graphql',
});

const port = 4000;

app.listen(port, () => {
  console.log(`🚀 Server is running at: http://localhost:${port}/graphql`);
});
Enter fullscreen mode Exit fullscreen mode
  • You can now save all your changes, run npm start and navigate to http://localhost:4000/graphql. You should see the following:

Playground

You have just launched GraphQL playground from your very first GraphQL server! In the left window, you can write your queries and mutations while responses will be displayed on the right-hand side! Let's give it a go and write our very first query:

First Query

  • Line#2 query AnyNameOfQuery - in here you simply specify whether you are about to use query or mutation and you are naming your query/mutation.
  • Line#3 tasks{ this is the name of our query specified in typeDefs file:
tasks: [Task!]!
Enter fullscreen mode Exit fullscreen mode
  • Line#4 & 5 specifies what are we interested in. We have specified in our typeDefs that query task is going to return an array of task objects.
  • Hit the play button in the middle of the playground and you should get the following response:

First Response

If you type in more than one query or mutation in the left window, play button should give you an option to choose which query/mutation you would like to execute.

When we execute the query task we have access to the full object, but we might only want a certain part of it! So in our example above, we have requested only title and description from an array of tasks. You might think, why would we set a name of our query if we are using the pre-named query from our typeDefs.
The answer is - we don't have to! But imagine you are working on a client and you want to access tasks twice, once where you are only interested in titles, and other time when you are interested in descriptions only! Naming queries can be very helpful.

  • Now, let's add assignedTo to our existing query, which is not going to work for now but I would like you to try it anyway to give you a better understanding the duty of the resolvers.

Add assignedTo{ and hit ctrl + space. You should see all the available data that GraphQL can fetch for you, all that information comes from types specified in typeDefs.

  • Let's say we are interested in firstName and lastName of our users.

AssignedTo

Hit play and...an error! Think of our resolvers and typeDefs now:

const { tasks, users } = require('../db');

const resolvers = {
  Query: {
    tasks() {
      return tasks;
    },

    users() {
      return users;
    },
  },
};

module.exports = resolvers;
Enter fullscreen mode Exit fullscreen mode

The reason why it is not working is that we must implement a new resolver to return the user that the task is assigned to.

  • Let's specify what our assignedTo should do by adding the following code to the resolvers:
const resolvers = {

    Task: {
        assignedTo(task) {
            return users.filter(u => u.id === task.assignedTo);
        },
    },

    Query: {
        tasks() {
            return tasks;
        },

        users() {
            return users;
        },
    },
};
Enter fullscreen mode Exit fullscreen mode

So, when assignedTo is accessed, we are going to filter through an array of users and return a user that has the id of matching assignedTo.

Now our query should work just fine and I recommend you to play a little bit with queries in the playground to get a better understanding of GraphQL.

  • Next, let's add one more query to our server - let's say we would like our server to accept a user name and return with a User object of that name. First, we need to edit our typeDefs.graphql:
type Query {
    tasks: [Task!]!
    users: [User!]!
    userByName(firstName: String!): User!
}
Enter fullscreen mode Exit fullscreen mode

So our new userByName query is going to take in a string and is going to return a User object to us.

  • Now into resolvers:
Query: {
        tasks() {
            return tasks;
        },

        users() {
            return users;
        },

        userByName(parent, args, context,){
            return users.filter(u => u.firstName === args.firstName)
        },
    },
Enter fullscreen mode Exit fullscreen mode

What we are doing is equivalent to REST params!

  • Now restart the server and test our new query in a playground:

Search for user by name

I think it would be a great practice for you to enable another query, let's say findUserById - give it a go yourself!

  • Next, we are going to add our first mutation type! It would be useful if we could add tasks to our database, to start it we need to edit our typeDefs first:
type Mutation {
    addTask(id: ID!, title: String!, description: String!, status: String!, assignedTo: ID!): Task!
}
Enter fullscreen mode Exit fullscreen mode

Our addTask mutation takes in an id, title, description, status, and assignedTo, all fields are required and we want to return new task.

  • Now to resolvers:
const { tasks, users } = require('../db');

const resolvers = {

    Task: {
        assignedTo(task) {
            return users.filter(u => u.id === task.assignedTo);
        },
    },

    Query: {
        tasks() {
            return tasks;
        },

        users() {
            return users;
        },

        userByName(parent, args, context,){
            return users.filter(u => u.firstName === args.firstName)
        }
    },
    Mutation: {
        addTask(parent, args, context) {
          const newTask = {
            id: args.id,
            title: args.title,
            description: args.description,
            status: args.status,
            assignedTo: args.assignedTo,
          };

            tasks.push(newTask);

            return newTask;
        },
    };
};

module.exports = resolvers;
Enter fullscreen mode Exit fullscreen mode

What we are doing in addTask mutation is:

  • Creating a new task based on passed in parameters
  • Push new task to the task array
  • Return the newly created task

You can view our newly created mutation in action by visiting the playground:

new task mutation

  • Our second mutation is going to be deleteTask mutation, again we start with typeDefs.graphql file:
removeTask(id: ID!): [Task!]!
Enter fullscreen mode Exit fullscreen mode
  • Next our resolvers:
const resolvers = {

    Task: {
        assignedTo(task) {
            return users.filter(u => u.id === task.assignedTo);
        },
    },

    Query: {
        tasks() {
            return tasks;
        },

        users() {
            return users;
        },

        userByName(parent, args, context,){
            return users.filter(u => u.firstName === args.firstName)
        }
    },
    Mutation: {
        addTask(parent, args, context) {
          const newTask = {
            id: args.id,
            title: args.title,
            description: args.description,
            status: args.status,
            assignedTo: args.assignedTo,
          };

            tasks.push(newTask);

            return newTask;
        },

        removeTask(parent, args, context) {
            const taskIndex = tasks.findIndex(t => t.id === args.id);

            tasks.splice(taskIndex, 1);

            return tasks;
          },
    }
};
Enter fullscreen mode Exit fullscreen mode

And just like with the first mutation give it a try in the playground!

Summary

I think by now you should have a good idea what you can do with GraphQL and what is the difference between GraphQL and REST - all those queries and mutations we went through used one endpoint and the client dictates what he wants from the server which can hugely improve the speed of our responses! Another huge success of GraphQL is that it allows receiving many resources in a single request! Imagine that on one of your pages you need access to both tasks and user - you can do it by sending one query! To me, understanding GraphQL changed the way I look at client-server architecture - simply because I'm finding it so amazing and easy to work with that I regret I only got to know it now! I really do hope you will enjoy it as well!

Now, let's head straight for our last part - absolutely mind-blowing Graphback!

Top comments (0)