Chapter 9: What is GraphQL?

In this chapter, we'll learn about GraphQL, one of the hottest web technologies currently being used. Many large companies have adopted GraphQL for their APIs, including companies such as Facebook, Twitter, New York Times, and GitHub. We'll learn what makes GraphQL so popular, how it works internally, and how we can take advantage of its features.

In this chapter, we're going to cover the following main topics:

  • Understanding GraphQL
  • Understanding GraphQL schemas
  • Understanding typedefs and resolvers
  • Understanding queries, mutations, and subscriptions

Technical requirements

You should have a basic understanding of web development using Node. We will once again be using Node and Visual Studio Code.

The GitHub repository is at https://github.com/PacktPublishing/Full-Stack-React-TypeScript-and-Node. Use the code in the Chap9 folder.

To set up the Chap9 code folder, go to your HandsOnTypescript folder and create a new folder called Chap9.

Understanding GraphQL

In this section, we will explore what GraphQL is, why it was created, and what problems it attempts to solve. It is important to understand the underlying reasons for GraphQL's existence as it will help us design better web APIs.

So, what is GraphQL? Let's list some of its main characteristics:

  • GraphQL is a data schema standard developed by Facebook.

    GraphQL provides a standard language for defining data, data types, and related data queries. You can think of GraphQL as roughly analogous to an interface that provides a contract. There's no code there, but you can still see what types and queries are available.

  • GraphQL works across platforms, frameworks, and languages.

    When we create an API using GraphQL, the same GraphQL language will be used to describe our data, its types, and queries no matter what programming language or operating system we use. Having a consistent and reliable representation of data across a wide variety of systems and platforms is, of course, a good thing for clients and systems. But it's also beneficial to programmers, since we can continue to use our normal programming language and frameworks of choice.

  • GraphQL returns control for what is queried to the caller.

    In a standard web service, it is the server that controls what fields of the data will be returned. However, in a GraphQL API, it is the client that determines which fields they would like to receive. This gives clients better control and reduces bandwidth usage and cost.

Broadly speaking, there are two main uses of a GraphQL endpoint. One is as a gateway to consolidate other data services, and the other is as the main web API service that directly receives data from a datastore and provides it to clients. Here's a diagram of GraphQL being used as a gateway for other data:

Figure 9.1 – GraphQL as a gateway

Figure 9.1 – GraphQL as a gateway

As you can see, GraphQL is acting as the single source of truth for all clients. It works well in this capacity due to its standards-based language that is supported across a wide variety of systems.

For our own application, we will use it as our entire web API, but it is possible to mix it in with existing web services so that GraphQL handles only a part of the service calls being made. This means you do not need to rewrite your entire application. You can introduce GraphQL slowly and deliberately where it makes sense to do so, without disrupting your current application services.

In this section, we took a look at GraphQL at a conceptual level. GraphQL has its own data language, meaning it can be used regardless of server framework, application programming language, or operating system. This flexibility allows GraphQL to be a powerful means of sharing data throughout an organization or even across the web. In the next section, we will explore the GraphQL schema language and see how it works. It will help us structure our data models and understand how to set up our GraphQL server.

Understanding GraphQL schemas

As stated, GraphQL is a language used to provide structure and type information to our entity data. Regardless of which vendor's implementation of GraphQL is used on the server, our client can expect the same data structures to be returned. This ability to abstract away the implementation details of servers to clients is one of the strengths of GraphQL.

Let's create a simple GraphQL schema and see what it looks like:

  1. In the Chap9 folder, create a new folder called graphql-schema.
  2. Open your terminal in that folder and then run this command, accepting the defaults:

    npm init

  3. Now install these packages:

    npm i express apollo-server-express @types/express

  4. Initialize TypeScript with this command:

    tsc -init

    Notice, after this command completes, that the default tsconfig.json setting is strict.

  5. Create a new TypeScript file called typeDefs.ts and add this to it:

    import { gql } from "apollo-server-express";

    This import gets the gql object, which allows syntax formatting and highlighting of the GraphQL schema language:

    const typeDefs = gql`

      type User {

        id: ID!

        username: String!

        email: String

      }

      type Todo {

        id: ID!

        title: String!

        description: String

      }

      type Query {

        getUser(id: ID): User

        getTodos: [Todo!]

      }

    `;

    The language is fairly simple and looks a lot like TypeScript. Starting from the top, first we have a User entity, as indicated by the type keyword. type is a GraphQL keyword that indicates that an object of a certain structure is being declared. As you can see, the User type has multiple fields. The id field is of type ID!. The ID type is a built-in type that indicates a unique value, basically a GUID of some kind. The exclamation mark indicates that the field cannot be null, whereas no exclamation mark would indicate that it can be null. Next, we see the username field and its type of String!, which of course means it is a non-nullable string type. Then, we have the description field, but it has a String type without an exclamation mark, so it is nullable.

    The Todos type has similar fields, but notice the Query type. This shows that even queries are types in GraphQL. So, if you look at the two queries, getUser and getTodos, you can see why we created the User and Todos types, as they become the return values for our two Query methods. Also notice that the getTodos function returns an array of non-nullable Todos, which is indicated by the brackets. Finally, we export our type definitions using the typeDefs variable:

    export default typeDefs;

Type definitions are used by Apollo GraphQL to describe the schema types in a schema file. Before your server can start providing any GraphQL data, it must first have a complete schema file that lists all of your application's types, their fields, and queries that will be served in its API.

Another thing to note is that GraphQL has several default scalar types that are built into the language. These are Int, Float, String, Boolean, and ID. As you noticed in the schema file, we did not have to create a type notation for these types.

In this section, we reviewed what a simple GraphQL schema file looks like. We will be using this syntax as we build out our API. In the next section, we will dive deeper into the GraphQL language and also learn what resolvers are.

Understanding Typedefs and Resolvers

In this section we will further explore GraphQL schemas, but we will also implement resolvers, which are the functions that do the actual work. This section will also introduce us to Apollo GraphQL and how to create a GraphQL server instance.

What are resolvers? Resolvers are the functions that get or edit the data from our datastore. This data is then matched with the GraphQL type definition.

In order to see what the role of resolvers is in more depth, we need to continue building out our previous project. Let's look at the steps:

  1. Install the dependency UUID. This tool will allow us to create a unique ID for our ID types:

    npm i uuid @types/uuid

  2. Create a new file called server.ts, which will start our server, with this code:

    import express from "express";

    import { ApolloServer, makeExecutableSchema } from "apollo-server-express";

    import typeDefs from "./typeDefs";

    import resolvers from "./resolvers";

    Here we import dependencies needed to set up our server. We already created the typeDefs file and we will soon create the resolvers file.

  3. Now we create our Express server app object:

    const app = express();

  4. makeExecutableSchema builds a programmatic schema from the combination of our typeDefs file and our resolvers file:

    const schema = makeExecutableSchema({ typeDefs, resolvers });

  5. Finally, we create an instance of our GraphQL server:

    const apolloServer = new ApolloServer({

      schema,

      context: ({ req, res }: any) => ({ req, res }),

    });

    apolloServer.applyMiddleware({ app, cors: false });

    context is made up of the request and response objects of Express. Then, we add our middleware, which for GraphQL is our Express server object called app. The cors option indicates to disable GraphQL from acting as our CORS server. We'll discuss CORS in later chapters as we build out our app.

    In this code, we are now starting up our Express server by listening on port 8000:

    app.listen({ port: 8000 }, () => {

      console.log("GraphQL server ready.");

    });

    The listen handler just logs a message to announce it has started.

Now let's create our resolvers:

  1. Create the resolvers.ts file and add this code to it:

    import { IResolvers } from "apollo-server-express";

    import { v4 } from "uuid";

    import { GqlContext } from "./GqlContext";

    interface User {

      id: string;

      username: string;

      description?: string;

    }

    interface Todo {

      id: string;

      title: string;

      description?: string;

    }

  2. Since we are using TypeScript, we want to use types to represent our returned objects, and that's what User and Todo represent. These types will be matched by GraphQL to the GraphQL types of the same name we had created in our typeDefs.ts file:

    const resolvers: IResolvers = {

      Query: {

        getUser: async (

          obj: any,

          args: {

            id: string;

          },

          ctx: GqlContext,

          info: any

        ): Promise<User> => {

          return {

            id: v4(),

            username: "dave",

          };

        },

    Here is our first resolver function, matching the getUser query. Notice that the parameter is more than just the id parameter. This is coming from the Apollo GraphQL server and adds additional information for our call. (Note that to save time, I am hardcoding a User object.) Also, we will create the GqlContext type later, but basically, it is a container that holds our request and response objects that we learned about in Chapter 8, Learning Server-Side Development with Node.js and Express.

  3. Similarly to getUser, our getTodos resolver receives similar parameters and also returns a hardcoded set of Todo:

        getTodos: async (

          parent: any,

          args: null,

          ctx: GqlContext,

          info: any

        ): Promise<Array<Todo>> => {

          return [

            {

              id: v4(),

              title: "First todo",

              description: "First todo description",

            },

            {

              id: v4(),

              title: "Second todo",

              description: "Second todo description",

            },

            {

              id: v4(),

              title: "Third todo",

            },

          ];

        },

  4. Then we export the resolvers object:

      },

    };

    export default resolvers;

    As you can see, our actual data getters are just normal TypeScript code. If we had used Java or C# or any other language, the resolvers would simply be Create Read Update Delete (CRUD) operations in those languages as well. The GraphQL server, then, is just translating the data entity models into the types in our type definition schema file.

  5. Now let's create our GqlContext type. Create a file called GqlContext.ts and add this code:

    import { Request, Response } from "express";

    export interface GqlContext {

      req: Request;

      res: Response;

    }

    This is just a simple shell interface that allows us to provide type safety to our context in our GraphQL resolver calls. As you can see, this type contains the Express Request and Response objects.

  6. So, now we need to compile our code to JavaScript since we are using TypeScript. Run this command:

    Tsc

    This will have created js versions of all the ts files.

  7. Now we can run our new code; enter this:

    nodemon server.js

  8. If you go to the URL http://localhost: 8000/graphql, you should see the GraphQL Playground screen. This is a query testing page provided by Apollo GraphQL that allows us to manually test our queries. It looks like this:
    Figure 9.2 – The GraphQL dev client

    Figure 9.2 – The GraphQL dev client

    Notice that I have already run one of the queries, which looks like JSON and is on the left, and the result is shown, which is also JSON and on the right. If you look at our query on the left, I am explicitly asking for only the id field, which is why only the id field is returned. Notice that the standard result format is data > <function name> > <fields>. Try running the getTodos query as a test.

  9. Another thing to note is the DOCS tab, which shows all the available queries, mutations, and subscriptions (we will go over these in the next section). It looks like this:
    Figure 9.3 – The DOCS tab

    Figure 9.3 – The DOCS tab

  10. Finally, the SCHEMA tab shows the schema type information of all our entities and queries:
Figure 9.4 – The SCHEMA tab

Figure 9.4 – The SCHEMA tab

As you can see, it looks identical to our typeDefs.ts file.

In this section, we took a look at resolvers by running a small GraphQL server. Resolvers are the other half that makes GraphQL actually function. We also saw how relatively easy it is to get a small GraphQL server running by using the Apollo GraphQL library.

In the next section, we will delve more deeply into queries by looking at mutations and subscriptions.

Understanding queries, mutations, and subscriptions

When creating a GraphQL API, we want to do more than just get data: we may also want to write to a datastore or be notified when some data changes. In this section, we'll see how to do both actions in GraphQL.

Let's take a look at how to write data using mutations first:

  1. We will create a mutation called addTodo, but in order to make the mutation more realistic, we will need a temporary datastore. So, we will create an in-memory datastore for testing purposes. Create the db.ts file and add this code to it:

    import { v4 } from "uuid";

    export const todos = [

      {

        id: v4(),

        title: "First todo",

        description: "First todo description",

      },

      {

        id: v4(),

        title: "Second todo",

        description: "Second todo description",

      },

      {

        id: v4(),

        title: "Third todo",

      },

    ];

    We have just added Todos from our previous list into an array that we are exporting.

  2. Now we need to update our typeDefs.ts file to include our new mutation. Update it like this:

    import { gql } from "apollo-server-express";

    const typeDefs = gql`

      type User {

        id: ID!

        username: String!

        email: String

      }

      type Todo {

        id: ID!

        title: String!

        description: String

      }

      type Query {

        getUser(id: ID): User

        getTodos: [Todo!]

      }

      type Mutation {

        addTodo(title: String!, description: String): Todo

      }

    `;

    export default typeDefs;

    As you can see, other queries remain the same, but we added a new type called Mutation, which is where any queries that change data will reside. We also added our new mutation called addTodo.

  3. Now we want to add our addTodo resolver. Add this code to your resolvers.ts file:

    Mutation: {

        addTodo: async (

          parent: any,

          args: {

            title: string;

            description: string;

          },

          ctx: GqlContext,

          info: any

        ): Promise<Todo> => {

          todos.push({

            id: v4(),

            title: args.title,

            description: args.description

          });

          return todos[todos.length - 1];

        },

      },

    As you can see, we have a new container object called Mutation, and inside of it is our addTodo mutation. It has similar parameters to the queries, but this mutation will add a new Todo to the todos array. If we run this code in the playground, we see this:

Figure 9.5 – The GraphQL playground for the addTodo mutation

Figure 9.5 – The GraphQL playground for the addTodo mutation

When our query is of type Query, we can leave out the query prefix. However, since this is a mutation, we must include it. As you can see, we only get back id and title, because that is all we asked for.

Now let's take a look at subscriptions, which are a way of being notified when certain data changes. Let's get notified when our addTodo adds a new Todo object:

  1. We need to add an object of type PubSub from the apollo-server-express library into the GraphQL server context. This object is what allows us to both subscribe (ask to be notified when changes occur) and publish (send a notification when changes occur). Update the server.ts file as follows:

    import express from "express";

    import { createServer } from "http";

    import {

      ApolloServer,

      makeExecutableSchema,

      PubSub,

    } from "apollo-server-express";

    import typeDefs from "./typeDefs";

    import resolvers from "./resolvers";

    First, we get an import of the PubSub type. Notice we also get createServer; we'll use that later.

  2. Here is our pubsub object, based on the PubSub type:

    const app = express();

    const pubsub = new PubSub();

  3. Now we add the pubsub object to the GraphQL server's context so that it can be used from our resolvers:

    const schema = makeExecutableSchema({ typeDefs, resolvers });

    const apolloServer = new ApolloServer({

      schema,

      context: ({ req, res }: any) => ({ req, res, pubsub }),

    });

  4. Create an httpServer instance from Node directly and then use the installSubscription Handlers function on it. Then, when we call listen, we are now calling listen on the httpServer object and not the app object:

    apolloServer.applyMiddleware({ app, cors: false });

    const httpServer = createServer(app);

    apolloServer.installSubscriptionHandlers(httpServer);

    httpServer.listen({ port: 8000 }, () => {

      console.log("GraphQL server ready." +

        apolloServer.graphqlPath);

      console.log("GraphQL subs server ready." +

        apolloServer.subscriptionsPath);

    });

  5. Now let's update our typeDefs.ts file to add our new mutation. Just add this type:

    type Subscription {

        newTodo: Todo!

      }

  6. Now we can update our resolvers.ts file with our new subscription resolver:

    import { IResolvers } from "apollo-server-express";

    import { v4 } from "uuid";

    import { GqlContext } from "./GqlContext";

    import { todos } from "./db";

    interface User {

      id: string;

      username: string;

      email?: string;

    }

    interface Todo {

      id: string;

      title: string;

      description?: string;

    }

    const NEW_TODO = "NEW TODO";

    Here we've created a new NEW_TODO constant to act as the name of our new subscription. Subscriptions require a unique label, sort of like a unique key, so that they can be correctly subscribed to and published:

    const resolvers: IResolvers = {

      Query: {

        getUser: async (

          parent: any,

          args: {

            id: string;

          },

          ctx: GqlContext,

          info: any

        ): Promise<User> => {

          return {

            id: v4(),

            username: "dave",

          };

        },

    As you can see, nothing in our query changes, but it's included here for completeness:

        getTodos: async (

          parent: any,

          args: null,

          ctx: GqlContext,

          info: any

        ): Promise<Array<Todo>> => {

          return [

            {

              id: v4(),

              title: "First todo",

              description: "First todo description",

            },

            {

              id: v4(),

              title: "Second todo",

              description: "Second todo description",

            },

            {

              id: v4(),

              title: "Third todo",

            },

          ];

        },

      },

    Again, our query remains the same:

      Mutation: {

        addTodo: async (

          parent: any,

          args: {

            title: string;

            description: string;

          },

          { pubsub }: GqlContext,

    Notice that in place of the ctx object, we have deconstructed it to just use the pubsub object, as it's the only one we need:

          info: any

        ): Promise<Todo> => {

          const newTodo = {

            id: v4(),

            title: args.title,

            description: args.description,

          };

          todos.push(newTodo);

          pubsub.publish(NEW_TODO, { newTodo });

    Here we have publish, which is a function to notify us when we have added a new Todo. Notice the newTodo object is being included in the publish call, so it can be provided to the subscriber later:

         return todos[todos.length - 1];

        },

      },

      Subscription: {

        newTodo: {

          subscribe: (parent, args: null, { pubsub }:       GqlContext) =>

            pubsub.asyncIterator(NEW_TODO),

    Here we subscribe to new Todo adds. Notice that our subscription newTodo is not a function. It's an object with a member called subscribe:

         },

      },

    };

    export default resolvers;

    The rest is the same as before.

  7. Let's try testing this. First, make sure you have compiled your code with tsc, started your server, and refreshed the playground. Then, open a new tab in the playground, enter this subscription, and click the play button:
Figure 9.6 – The newTodo subscription

Figure 9.6 – The newTodo subscription

When you click the play button, nothing happens, because a new Todo has not been added yet. So, let's go back to our addTodo tab and add a new Todo. Once you've done that, come back to the newTodo tab and you should see this:

Figure 9.7 – The newTodo subscription result

Figure 9.7 – The newTodo subscription result

As you can see, that works, and we get the newly added Todo.

In this section, we learned about GraphQL queries, mutations, and subscriptions. We will be using these to build out our application API. Because GraphQL is an industry standard, all GraphQL client frameworks can work with any vendor's GraphQL server framework. Furthermore, clients using a GraphQL API can expect consistent behavior and the same query language regardless of server or vendor. This is the power of GraphQL.

Summary

In this chapter, we explored the power and capabilities of GraphQL, one of the hottest new technologies for creating web APIs. GraphQL is an extremely capable technology, but also, because it is an industry standard, we can always expect consistent behavior across servers, frameworks, and languages.

In the next chapter, we will start bringing together the technologies we've learned about thus far and create an Express server using TypeScript, GraphQL, and helper libraries.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.23.127.197