GraphQL basics. Part 1. Queries and mutations

GraphQL basics. Part 1. Queries and mutations

GraphQL is a query language for APIs and a server-side runtime. GraphQL is not tied to any particular database or storage engine. A GraphQL service is created by defining types and fields for those types, and then providing functions for each field. We will use the open API of the fictional ski resort "Snow Fang". Clicking on the link https://snowtooth.moonhighway.com/ you will see the GraphQL Playground. The interface is divided into two parts. On the left, a query is written, on the right, the result is displayed in JSON format. There are also DOCS and SCHEMA tabs. The DOCS tab displays all available queries, mutations and subscriptions. The SCHEMA tab displays the types and their fields.

Queries

This is what a simple query looks like:

{
  allTrails {
    name
  }
}

In this example, the names of all the trailsof the ski resort are requested. The query starts with the keyword query, followed by curly brackets, in which the required fields are written. In this case, the AllTrails field is requested, which is an object. For objects, you must define a list of subfields. In this case, we are requesting the name subfield. GraphQL queries can navigate through related objects and their fields, allowing clients to retrieve multiple related data in a single query, instead of performing multiple queries as would be necessary in a classic REST architecture.

{
  allTrails {
    name
    accessedByLifts {
      name
    }
  }
}

In this example, for each trail, a list of lifts is requested that can be used to climb this trail. GraphQL queries look the same for both individual items and lists of items, but we know what to expect based on what is specified in the schema.

Arguments

GraphQL allows you to pass arguments to fields.

{
  allTrails(status: OPEN) {
    name
  }
}

This example queries all open trails. For this, the value OPEN is passed to the argument status. The list of available arguments is displayed in the schema (tab SCHEMA).

Aliases

The result field matches the field name in the request. To request the same field with different arguments, you need to use aliases. They allow you to rename the result field.

{
  openTrails: allTrails(status: OPEN) {
    name
  }
  closedTrails: allTrails(status: CLOSED) {
    name
  }
}

In this example, open and closed trails are requested separately. The result field for open trails is openTrails, for closed trails - closedTrails.

Fragments

Fragments are collections of fields that can be included in queries. A fragment is defined by the keyword fragment.

fragment trailFields on Trail {
  name
  difficulty
  night
}

{
  openTrails: allTrails(status: OPEN) {
    ...trailFields
  }
  closedTrails: allTrails(status: CLOSED) {
    ...trailFields
  }
}

In this example, we define a trailFields fragment containing part of the Trail field subfields and use this fragment to request open and closed traces. To use a fragment, you must put an ellipsis in front of its name.

Operation name

Until now, we have used a shorthand syntax in which the type of operation and its name have been omitted, but in real applications it is useful to use them to make the code more readable.

query TrailAndLifts {
  allTrails {
    name
    difficulty
    night
    accessedByLifts {
      name
      capacity
    }
  }
}

There are three types of operations: query, mutation and subscription. In this example, we define a query called TrailAndLifts that returns a list of trails and their corresponding lifts. The operation name is only required in multi-op documents, but its use is encouraged as it is very useful for server-side debugging and logging.

Variables

So far, we've been writing all of our arguments inside the query string. To pass variables dynamically, after the operation name, in parentheses, define the list of variables (the name of the variable and its type). Variable names must start with a $ character. These variables can then be used as argument values for fields and subfields. To pass the value of a variable in the GraphQL Playground runtime, you need to define an object in JSON format in the VARIABLES tab, in which you specify the name of the variables and their values.

Query:

query Trails($trailStatus: TrailStatus) {
  allTrails(status: $trailStatus) {
    name
  }
}

Variables:

{
  "trailStatus": "OPEN"
}

In this example, we are passing the trailStatus variable with the value OPEN to get the names of all open trails. Variable definitions can be optional or required. To declare a variable as required, you need to put the "!" after its type, for example "Boolean!"

Default variables

The variable can have a default value. To do this, you need to add it after the declaration of the variable type:

query Trails($trailStatus: TrailStatus = OPEN) {
  allTrails(status: $trailStatus) {
    name
    difficulty
    night
  }
}

In this example, the default value for the trailStatus variable is OPEN. If all variables have default values, you can execute the query without passing any variables. If any variables are passed, they override the default values.

Directives

Using variables, you can change the structure of the query:

Query:

query Trails($includeLifts: Boolean!) {
  allTrails {
    name
    difficulty
    night

    accessedByLifts @include(if: $includeLifts) {
      name
    }
  }
}

Variables:

{
  "includeLifts": true
}

In this example, using the variable includeLifts, it is determined whether to include information about the elevators of the track in the query. For this, we used the @include directive. The directive can be attached to a field or to the inclusion of a fragment. The base GraphQL specification includes two directives:

  1. @include (if: Boolean). Include this field in the query if the argument is true.
  2. @skip (if: Boolean). Omit this field if the argument is true.

Mutations

Technically, any query can write data. However, it is useful to establish a convention that any operations that cause a write must be dispatched explicitly via mutation. As with queries, if the mutation field returns the type of an object, you can query nested fields. This can be useful for getting the new state of an object after an update.

mutation closeTrail {
  setTrailStatus(id: "parachute", status: CLOSED) {
    id
    status
  }
}

In this example, we send a request to close the trail and get a new trail status to make sure it is closed.

Multiple fields in mutations

A mutation, like a query, can contain several fields. There is one important difference between queries and mutations: while query fields are executed in parallel, mutation fields are executed sequentially, one after the other. This means that if we send two mutations in one request, the first is guaranteed to complete before the second starts.

Inline fragments

GraphQL allows you to define unions. If you are asking for a field that returns a union type, you will need to use inline fragment to access the data.

query searchOpenTrailsAndLifts {
  search(status:OPEN) {
    ... on Trail {
      name
      difficulty
    }
    ... on Lift {
      name
      capacity
    }
  }
}

In this example, we run a query to find all open trails and lifts and ask for different data depending on which type the query returns (Trail or Lift).

Meta fields

There are situations where you don't know what type you will get back from the GraphQL service. In doing so, you need to somehow determine how to handle this data on the client. GraphQL allows you to query the __typename meta field, which contains the name of the object's type.

query searchOpenTrailsAndLifts {
  search(status:OPEN) {
    ... on Trail {
      __typename
      name
    }
    ... on Lift {
      __typename
      name
    }
  }
}

In this example, the search returns the union type, which can be one of two options. It would be impossible to distinguish different types on the client without the __typename field.