GraphQL Details: What, How, and Why

Original author: Ryan Glover
  • Transfer
  • Tutorial
GraphQL is now, without exaggeration, this is the last peep of IT-mode. And if you do not yet know what kind of technology it is, how to use it, and why it may be useful to you, then the article we are publishing today is written for you. Here we will go over the basics of GraphQL using an example of a data schema implementation for the API of a popcorn company. In particular, let's talk about data types, queries and mutations.



What is GraphQL?


GraphQL is a query language used by client applications to work with data. GraphQL is associated with such a concept as a “scheme” - this is what allows you to organize the creation, reading, updating and deletion of data in your application (that is, we have four basic functions used when working with data warehouses, which are usually referred to by the acronym CRUD - create, read, update, delete).

It was said above that GraphQL is used to work with data in “your application”, and not “in your database”. The fact is that GraphQL is a system independent of data sources, that is, it does not matter where it is organized to organize its work.

If you look, without knowing anything about GraphQL, on the name of this technology, it may seem that we are faced with something very complicated and confusing. The name of the technology has the word “Graph”. Does this mean that in order to master it, you have to learn to work with graph databases? And the fact that the name contains “QL” (which can mean “query language”, that is, “query language”), does it mean that those who want to use GraphQL will have to learn a completely new programming language?

These fears are not entirely justified. In order to reassure you - this is the cruel truth about this technology: it is just embellished GETorPOSTinquiries. While GraphQL, in general, introduces some new concepts related to data organization and interaction with it, the internal mechanisms of this technology rely on the good old HTTP requests.

Rethinking REST Technology


Flexibility is what sets GraphQL technology apart from the well-known REST technology. When using REST, if everything is done correctly, endpoints are usually created taking into account the characteristics of a certain resource or application data type.

For example, when performing an GET-query to an endpoint /api/v1/flavors, it is expected that it will send a response that looks something like this:

[
  {
   "id": 1,
    "name": "The Lazy Person's Movie Theater",
    "description": "That elusive flavor that you begrudgingly carted yourself to the theater for, now in the comfort of your own home, you slob!"
  }, {
    "id": 2,
    "name": "What's Wrong With You Caramel",
    "description": "You're a crazy person that likes sweet popcorn. Congratulations."
  }, {
    "id": 3,
    "name": "Gnarly Chili Lime",
    "description": "The kind of popcorn you make when you need a good smack in the face."}
]

There is nothing catastrophically wrong with this answer, but let's think about the user interface, or rather, how we intend to consume this data.

If we want to display a simple list in the interface that contains only the names of the available types of popcorn (and nothing else), then this list may look like the one shown below.


The list of types of popcorn

It can be seen that here we are in a difficult situation. We may well decide not to use the field description, but are we going to sit back and pretend that we did not send this field to the client? What else can we do? And when, after a few months, they will ask us why the application is so slow for users, we just have to let the guy and no longer meet with the management of the company for which we made this application.

In fact, the fact that the server sends unnecessary data in response to a client request is not entirely our fault. REST is a data acquisition mechanism that can be compared to a restaurant in which the waiter asks the visitor: “What do you want?”, And, not particularly paying attention to his wishes, he tells him: “I will bring you what we have” .

If we throw aside jokes, then in real applications this can lead to problem situations. For example, we can display various additional information about each type of popcorn, such as price information, information about the manufacturer or nutritional information (“Vegan Popcorn!”). At the same time, inflexible REST endpoints make it very difficult to obtain specific data on specific types of popcorn, which leads to unreasonably high load on systems and to the fact that the resulting solutions are far from those that developers could be proud of.

How GraphQL Technology Improves What REST Technology Was Used For


A superficial analysis of the situation described above may seem that we are only a minor problem. “What's wrong with sending the client unnecessary data?” In order to understand the extent to which “unnecessary data” can be a big problem, remember that GraphQL was developed by Facebook. This company has to serve millions of requests per second.

What does it mean? And the fact that with such volumes every little thing matters.

GraphQL, if we continue the analogy with a restaurant, instead of “carrying” to the visitor “what is”, brings exactly what the visitor orders.

We can get a response from GraphQL that focuses on the context in which the data is used. In this case, we do not need to add “one-time” access points to the system, perform many requests or write multi-storey conditional structures.

How does GraphQL work?


As we have already said, GraphQL relies on simple GETor POST-queries to transmit data to the client and receive it from it . If we consider this idea in more detail, it turns out that there are two types of queries in GraphQL. The first type includes requests for reading data, which in GraphQL terminology are simply called queries and refer to the letter R (reading, reading) of the acronym CRUD. Queries of the second kind are data modification requests, which are called mutations in GraphQL. They relate to the axle boxes C, U, and D of the acronym CRUD, that is, they use them to create, create, update, and delete records.

All these queries and mutations are sent to the URL of the GraphQL server, which, for example, may look like https://myapp.com/graphql, in the form GETorPOST-queries. We will talk more about this below.

GraphQL Queries


GraphQL queries are entities representing a request to the server to receive certain data. For example, we have a certain user interface that we want to fill with data. For this data, we turn to the server, executing the request. When using traditional REST APIs, our request takes the form of a GET request. When working with GraphQL, a new query syntax is used:

{
  flavors {
    name
  }
}

Is that JSON? Or a JavaScript object? Neither one nor the other. As we have already said, in the name of the GraphQL technology, the last two letters, QL, mean “query language”, that is, the query language. This is, literally, a new language for writing data requests. All this sounds like a description of something rather complicated, but in fact there is nothing complicated here. Let's analyze the above query:

{
  // Сюда помещают описания полей, которые нужно получить.
}

All requests begin with a “root request”, and what you need to get during the execution of the request is called a field. In order to save yourself from confusion, it is best to call these entities "query fields in the schema." If such a name seems incomprehensible to you - wait a bit - below we will talk more about the scheme. Here we, in the root query, request a field flavors.

{
  flavors {
    // Вложенные поля, которые мы хотим получить для каждого значения flavor.
  }
}

When requesting a certain field, we must also indicate the nested fields that need to be received for each object that comes in response to the request (even if it is expected that only one object will come in response to the request).

{
  flavors {
    name
  }
}

What will be the result? After we send such a request to the GraphQL server, we will get a well-formed neat answer like the following:

{
  "data": {
    "flavors": [
      { "name": "The Lazy Person's Movie Theater" },
      { "name": "What's Wrong With You Caramel" },
      { "name": "Gnarly Chili Lime" }
    ]
  }
}

Please note that there is nothing superfluous. In order to make it clearer, here is another request that is executed to obtain data on another page of the application:

{
  flavors {
    id
    name
    description
  }
}

In response to this request, we get the following:

{
  "data": {
    "flavors": [
      { "id": 1, "name": "The Lazy Person's Movie Theater", description: "That elusive flavor that you begrudgingly carted yourself to the theater for, now in the comfort of your own home, you slob!" },
      { "id": 2, "name": "What's Wrong With You Caramel", description: "You're a crazy person that likes sweet popcorn. Congratulations." },
      { "id": 3, "name": "Gnarly Chili Lime", description: "A friend told me this would taste good. It didn't. It burned my kernels. I haven't had the heart to tell him." }
    ]
  }
}

As you can see, GraphQL is a very powerful technology. We turn to the same endpoint, and the answers to the requests exactly correspond to what is needed to fill the page from which these requests are executed.

If we need to get only one object flavor, then we can take advantage of the fact that GraphQL can work with arguments:

{
  flavors(id: "1") {
    id
    name
    description
  }
}

Here we rigidly set the specific identifier ( id) of the object in the code , the information about which we need, but in such cases we can use dynamic identifiers:

query getFlavor($id: ID) {
  flavors(id: $id) {
    id
    name
    description
  }
}

Here, in the first line, we give the request a name (the name is chosen arbitrarily, getFlavoryou can replace it with something like pizza, and the request remains operational) and declare the variables that the request expects. In this case, it is assumed that the identifier ( id) of the scalar type will be passed to the request ID(we will talk about types below).

Regardless of whether static or dynamic is idused when executing the request, here is what the response to such a request will look like:

{
  "data": {
    "flavors": [
      { "id": 1, "name": "The Lazy Person's Movie Theater", description: "That elusive flavor that you begrudgingly carted yourself to the theater for, now in the comfort of your own home, you slob!" }
    ]
  }
}

As you can see, everything is arranged very conveniently. You are probably starting to think about using GraphQL in your own project. And, although what we have already talked about looks wonderful, the beauty of GraphQL really manifests itself where it works with nested fields. Suppose that in our scheme there is another field that is called nutritionand contains information about the nutritional value of different types of popcorn:

{
  flavors {
    id
    name
    nutrition {
      calories
      fat
      sodium
    }
  }
}

It may seem that in our data warehouse, in each object flavor, a nested object will be contained nutrition. But it is not so. Using GraphQL, you can combine calls to independent, but related data sources in a single query, which allows you to receive answers that provide the convenience of working with embedded data without the need to denormalize the database:

{
  "data": {
    "flavors": [
      {
        "id": 1,
        "name": "The Lazy Person's Movie Theater",
        "nutrition": {
          "calories": 500,
          "fat": 12,
          "sodium": 1000
        }
      },
      ...
    ]
  }
}

This can significantly increase the productivity of the programmer and the speed of the system.

So far, we have talked about read requests. What about data update requests? Do using them give us the same convenience?

GraphQL mutations


While GraphQL queries load data, mutations are responsible for making changes to the data. Mutations can be used in the form of the basic RPC (Remote Procedure Call) mechanism for solving various tasks, such as sending user data to a third-party API.

When describing mutations, a syntax is used that resembles the one we used when generating queries:

mutation updateFlavor($id: ID!, $name: String, $description: String) {
  updateFlavor(id: $id, name: $name, description: $description) {
    id
    name
    description
  }
}

Here we declare a mutation updateFlavor, indicating some variables - id, nameand description. Acting according to the same scheme that is used to describe queries, we “draw up” variable fields (root mutation) using a keyword mutation, followed by a name describing the mutation, and a set of variables that are needed to form the corresponding data change request.

These variables include what we are trying to change, or what mutation we want to cause. Please also note that after the mutation, we can request the return of some fields.

In this case, we need to get, after changing the record, field id, nameanddescription. This can come in handy when developing something like optimistic interfaces, eliminating the need to fulfill a request to receive changed data after changing it.

Designing a schema and connecting it to a GraphQL server


So far, we have talked about how GraphQL works on the client, and how they execute queries. Now let's talk about how to respond to these requests.

▍GraphQL server


In order to execute a GraphQL query, you need a GraphQL server to which you can send such a query. A GraphQL server is a regular HTTP server (if you write in JavaScript, it can be a server created using Express or Hapi), to which a GraphQL diagram is attached.

import express from 'express'
import graphqlHTTP from 'express-graphql'
import schema from './schema'
const app = express()
app.use('/graphql', graphqlHTTP({
  schema: schema,
  graphiql: true
}))
app.listen(4000)

By “joining” a scheme, we mean a mechanism that passes requests received from the client through the scheme and returns answers to it. It is like an air filter through which air enters the room.

The process of "filtering" is associated with requests or mutations sent by the client to the server. Both queries and mutations are resolved using functions related to the fields defined in the root query or in the root mutation of the schema.

The above is an example HTTP server framework created using the Express JavaScript library. Using the function graphqlHTTPfrom the packageexpress-graphqlfrom Facebook, we “attach” the scheme (it is assumed that it is described in a separate file) and start the server on port 4000. That is, clients, if we talk about the local use of this server, will be able to send requests to the address http://localhost:4000/graphql.

▍ Data types and resolvers


In order to ensure the operation of the GraphQL server, you need to prepare the schema and attach it to it.

Recall that we talked about declaring fields in a root query or in a root mutation above.

import gql from 'graphql-tag'
import mongodb from '/path/to/mongodb’ // Это - лишь пример. Предполагается, что `mongodb` даёт нам подключение к MongoDB.
const schema = {
  typeDefs: gql`
    type Nutrition {
      flavorId: ID
      calories: Int
      fat: Int
      sodium: Int
    }
    type Flavor {
      id: ID
      name: String
      description: String
      nutrition: Nutrition
    }
    type Query {
      flavors(id: ID): [Flavor]
    }
    type Mutation {
      updateFlavor(id: ID!, name: String, description: String): Flavor
    }
  `,
  resolvers: {
    Query: {
      flavors: (parent, args) => {
        // Предполагается, что args равно объекту, наподобие { id: '1' }
        return mongodb.collection('flavors').find(args).toArray()
      },
    },
    Mutation: {
      updateFlavor: (parent, args) => {
        // Предполагается, что args равно объекту наподобие { id: '1', name: 'Movie Theater Clone', description: 'Bring the movie theater taste home!' }
        // Выполняем обновление.
        mongodb.collection('flavors').update(args)
        // Возвращаем flavor после обновления.
        return mongodb.collection('flavors').findOne(args.id)
      },
    },
    Flavor: {
      nutrition: (parent) => {
        return mongodb.collection('nutrition').findOne({
          flavorId: parent.id,
        })
      }
    },
  },
}
export default schema

The definition of fields in the GraphQL schema consists of two parts - from type declarations ( typeDefs) and recognizers ( resolver). The entity typeDefscontains type declarations for the data used in the application. For example, earlier we talked about a request to get a list of objects from the server flavor. In order to make a similar request to our server, you need to do the following three steps:

  1. Tell the schema about how the object data looks flavor(in the example above, it looks like a type declaration type Flavor).
  2. Declare a field in the root field type Query(this is a flavorsvalue property type Query).
  3. Declare an object recognizer function resolvers.Querywritten in accordance with the fields declared in the root field type Query.

Let us now pay attention to typeDefs. Here we give the schema information about the shape of our data. In other words, we tell GraphQL about the various properties that may be contained in entities of the corresponding type.

type Flavor {
  id: ID
  name: String
  description: String
  nutrition: Nutrition
}

Advertisement type Flavorindicates that the object flavormay contain a field idtype ID, field nametypes String, field descriptiontypes Stringand field nutritiontypes Nutrition.

In the case of, nutritionwe use here the name of another type declared in typeDefs. Here, the design type Nutritiondescribes a form of nutritional data for popcorn.

Pay attention to the fact that here, as at the very beginning of this material, we are talking about an “application” and not about a “database”. In the above example, it is assumed that we have a database, but the data in the application can come from any source. It could even be a third-party API or a static file.

Just as we did in the adtype Flavor, here we indicate the names of the fields that will be contained in the objects nutrition, using, as the data types of these fields (properties), what in GraphQL is called scalar data types. At the time of this writing, GraphQL supported 5 built-in scalar data types :

  • Int: signed 32-bit integer.
  • Float: signed double-precision floating-point number.
  • String: UTF-8 encoded character sequence.
  • Boolean: boolean trueor false.
  • ID: A unique identifier that is often used to repeatedly load objects or as a key in the cache. Values ​​of a type are IDserialized in the same way as strings, however, an indication that a type has a value is IDemphasized by the fact that this value is not intended to be shown to people, but for use in programs.

In addition to these scalar types, we can also assign properties to types that we define ourselves. That is what we have done by assigning the property nutritiondescribed in the design type Flavortype Nutrition.

type Query {
  flavors(id: ID): [Flavor]
}

In the construct type Querythat describes the root type Query(the “root query” we talked about earlier), we declare the name of the field that may be requested. By declaring this field, we, in addition, together with the data type that we expect to return, specify the arguments that may come in the request.

In this example, we expect a possible argument of a idscalar type ID. In response to such a request, an array of objects is expected whose device resembles a device of type Flavor.

▍Connecting the query recognizer


Now that type Querythere is a field definition in the root field, we need to describe what is called a recognizer function.

This is where GraphQL, more or less, “stops”. If we look at the resolversschematic object , and then at the object Queryembedded in it, we can see there the property flavorsto which the function is assigned. This function is called the resolver for the field flavors, which is declared in the root type Query.

typeDefs: gql`…`,
resolvers: {
  Query: {
    flavors: (parent, args) => {
      // Предполагается, что args равно объекту наподобие { id: '1' }
      return mongodb.collection('flavors').find(args).toArray()
    },
  },
  …
},

This recognizer function takes several arguments. An argument parentis the parent request, if one exists, the argument is argsalso passed to the request if it exists. An argument can also be used here context, which is not presented in our case. It makes it possible to work with various “contextual” data (for example, with information about the current user in case the server supports the user account system).

Inside the recognizer, we do everything we need in order to fulfill the request. This is where GraphQL "stops worrying" about what is happening and allows us to load and return data. Here, we repeat, you can work with any data sources.

Although GraphQL is not interested in the sources of data input, this system is extremely interested in what we return. We can return a JSON object, an array of JSON objects, a promise (its resolution is taken by GraphQL).

Here we use a mock call to flavorsthe MongoDB database collection , passing args(if the corresponding argument is passed to the resolver) to the call .find()and returning what will be found as a result of making this call as an array.

▍Getting data for nested fields


Above, we have already sorted out something related to GraphQL, but for now, perhaps, it is not yet clear how to deal with a nested field nutrition. Remember that Nutritionwe do not actually store the data represented by the field together with the main data describing the entity flavor. We assume that this data is stored in a separate collection / database table.

Although we told GraphQL that it type Flavormight include data nutritionin the form type Nutrition, we did not explain to the system how to get this data from the store. This data, as already mentioned, is stored separately from these entities flavor.

typeDefs: gql`
    type Nutrition {
      flavorId: ID
      calories: Int
      fat: Int
      sodium: Int
    }
    type Flavor {
      […]
      nutrition: Nutrition
    }
    type Query {…}
    type Mutation {…}
  `,
  resolvers: {
    Query: {
      flavors: (parent, args) => {…},
    },
    Mutation: {…},
    Flavor: {
      nutrition: (parent) => {
        return mongodb.collection('nutrition').findOne({
          flavorId: parent.id,
        })
      }
    },
  },

If you look closely at the object resolversin the diagram, you will notice that there are nested objects Query, Mutationand Flavor. They correspond to the types that we declared above at typeDefs.

If you look at the object Flavors, it turns out that the field nutritionin it is declared as a recognizer function. A notable feature of this solution is the fact that we declare a function directly in the type Flavor. In other words, we tell the system: "We want you to load the field nutritionjust like that for any query that uses type Flavor."

In this function, we execute a regular request to MongoDB, but here pay attention to the fact that we use the argumentparentpassed to the resolver function. What is presented here by argument parentis what is contained in the fields available in flavors. For example, if we need all entities flavor, we will execute the following query:

{
  flavors {
    id
    name
    nutrition {
      calories
    }
  }
}

Each field flavorreturned from flavors, we will pass through the recognizer nutrition, and this value will be represented by an argument parent. If you look closely at this construction, it turns out that we, in a request to MongoDB, use a field parent.idthat represents the identities flavorthat we are currently processing.

The identifier is parent.idtransmitted in a query to the database, where a record is searched nutritionwith an identifier flavorIdthat corresponds to the entity being processed flavor.

▍Connection of mutations


What we already know about queries is well tolerated by mutations. In fact, the process of preparing mutations almost completely coincides with the process of preparing queries. If you look at the root entity type Mutation, you can see that we declared a field in it updateFlavorthat accepts the arguments specified on the client.

type Mutation {
  updateFlavor(id: ID!, name: String, description: String): Flavor
}

Этот код можно расшифровать так: «Мы ожидаем, что мутация updateFlavor принимает id типа ID (восклицательный знак, !, сообщает GraphQL о том, что это поле необходимо), name типа String и description типа String». Кроме того, после завершения выполнения мутации мы ожидаем возврат некоторых данных, структура которых напоминает тип Flavor (то есть — объект, который содержит свойства id, name, description, и, возможно, nutrition).

{
  typeDefs: gql`…`,
  resolvers: {
    Mutation: {
      updateFlavor: (parent, args) => {
        // Предполагается, что args равно объекту наподобие { id: '1', name: 'Movie Theater Clone', description: 'Bring the movie theater taste home!' }
        // Выполняем обновление.
        mongodb.collection('flavors').update(
          { id: args.id },
          {
            $set: {
              ...args,
            },
          },
        )
        // Возвращаем flavor после обновления.
        return mongodb.collection('flavors').findOne(args.id)
      },
    },
  },
}

Inside the recognizer function for mutation, updateFlavorwe do exactly what we can expect from a similar function: we organize interaction with the database in order to change in it, that is, to update, information about the essence of interest to us flavor.

Please note that immediately after the update is completed, we update the database in order to find the same entity again flavorand return it from the recognizer. Why is this so?

Remember that on the client we expect to receive the object in the state it was in after the mutation was completed. In this example, we expect that the entity flavorwe just updated will be returned .

Is it possible to simply return an objectargs? Yes you can. The reason why we decided not to do this in this case is because we want to be 100% sure that the update operation in the database was successful. If we read from the database information that should be changed, and it turns out that it really has been changed, then we can conclude that the operation was successful.

Why might you need to use GraphQL?


Although what we just worked on does not look particularly large-scale, now we have a functioning, albeit simple, GraphQL-API.

As is the case with any new technology, after your first acquaintance with GraphQL, you may wonder why you might need something like this. Honestly, this question cannot be given a clear and simple answer. There are so many things to consider in order to find such an answer. And you can, by the way, think about instead of GraphQL simply choosing the REST technology proven by time or directly accessing the database. As a matter of fact, here are a few ideas that you should reflect on in your search for the answer to the question of whether you need GraphQL technology.

▍You seek to reduce the number of requests made from the client


Many applications suffer from having to perform too many HTTP requests, from having to do this too often, and from being complex requests. While the use of GraphQL technology does not completely refuse to execute queries, this technology, if used correctly, can significantly reduce the number of queries executed by the client (in many cases, just one query is enough to get a certain set of related data).

Whether your project is an application with many users, or an application that processes huge amounts of data (for example, it is something like a system for working with medical data), using GraphQL will definitely improve the performance of its client side.

▍You want to avoid data denormalization, carried out only in order to optimize the work of user interface building mechanisms


In applications that use large amounts of relational data, a “denormalization trap” can often occur. Although this approach turns out to be working, it is, without a doubt, far from ideal. Its use may adversely affect system performance. By using GraphQL and nested queries, the need for data denormalization is greatly reduced.

▍ You have many sources of information that you access from different applications


This problem can be partially solved using traditional REST APIs, but even with this approach, one problem still remains: uniformity of requests made from the client side. Suppose your project includes a web application, applications for iOS and Android, as well as an API for developers. In such circumstances, you will most likely have to, on each platform, “craft from improvised materials” means for fulfilling requests.

This leads to the fact that it is necessary to support, on different platforms, several implementations of HTTP, this means the lack of uniformity in the means of query execution and the presence of confusing API endpoints (you probably already saw this).

▍Maybe GraphQL technology is the top of excellence? Should I throw away my REST API right now and go to GraphQL?


Of course not. Nothing is perfect. And, it should be noted that working with GraphQL is not so simple. In order to create a working GraphQL schema, you need to perform many required steps. Since you are just studying this technology, this can lead you off balance, as it is not easy to understand what exactly is missing in your circuit for the correct operation of the system. However, error messages that occur on the client and on the server may not be particularly useful.

Further, the use of GraphQL on the client, which goes beyond the scope of the query language, is not standardized. Although working with GraphQL can be facilitated by various libraries, the most popular of which are Apollo and Relay, each of them has its own specific features.

GraphQL is also just a specification. Packages like graphql(this package is used inside the package express-graphqlused in our example) are just implementations of this specification. In other words, different GraphQL implementations for different programming languages ​​may interpret the specification in different ways. This can lead to problems, whether we are talking about a single developer, or about a team in which, when working on different projects, different programming languages ​​are used.

Summary


Although implementing GraphQL can be a daunting task, this technology represents an impressive step forward in the field of data processing. GraphQL cannot be called a cure for all diseases, but it is definitely worth experimenting with this technology. You can start, for example, by thinking about the most confusing and untidy subsystem used in your project when working with data, and trying to implement this subsystem using GraphQL.

By the way, here I have good news for you: GraphQL can be implemented incrementally. In order to benefit from the use of this technology, there is no need to translate absolutely everything into GraphQL. So, gradually introducing GraphQL into the project, you can deal with this technology yourself, interest the team, and if everything works out, everyone is happy, move on.

The main thing is to remember that GraphQL is, after all, just a tool. The use of GraphQL does not mean the need for a complete redesign of everything that was before. It should be noted that GraphQL is a technology that is definitely worth getting to know. Many people should think about the application of this technology in their projects. In particular, if your projects do not seem particularly productive, if you are developing complex interfaces, such as control panels, news feeds or user profiles, then you already know where exactly you can try GraphQL.

Dear readers! If your first acquaintance with GraphQL took place today, please tell us about whether you plan to use this technology in your projects.


Also popular now: