Fetching and changing data in GraphQL

There are many ways to send a request to fetch or change data with GraphQL, for example a raw curl requests, the graphiQL web UI or clients such as Apollo Client, Urql, React Query + GraphQL Request, React Query + Axios, React Query + Fetch API. In this article we look rather at the syntax of sending graphQL requests instead of using those libraries to actually send them.

A GraphQL Query that requests the field hero and its containing field name looks like this:

{
  hero {
    name
  }
}

A curl to send this could look like this:

curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "{ hero { name } }"}' \
http://localhost:4000/graphql

The returned data:

{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

The query can also fetch related data using sub-selections. In the following example an array of friends is returned:

{
  hero {
    name
    # Queries can have comments!
    friends {
      name
    }
  }
}
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

Arguments in a Query

human and height each are passed an argument:

{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}
{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 5.6430448
    }
  }
}

Here we passed “1000” as a fixed string. In a real app this should be dynamic of course. See below on how to use argument variables.

Aliases

Aliases let you rename the result of a field to anything you want. You need this to prevent a response name conflict, for example requesting field hero twice but with different arguments:

{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

Fragments

Instead of repeating fields in your query over and over you should use a Fragment instead and define on which object you want to use it:

{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}

You can use variables inside fragments:

query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}

Operation name

In production apps it’s useful to use operation name and type explicitly to make debugging and server-side logging easier. query is the operation type and HeroNameAndFriends is the operation name:

query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}

Variables in arguments

In your app you need to pass dynamic variables to your GraphQL queries instead of hard-coding a value of “1000” for example. So first you write your query specifying your variable names:

query HeroNameAndFriends($episode: Episode = JEDI) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

Second, you specify your variable values separately:

{
  "episode": "NEWHOPE"
}

All declared variables must be either scalars, enums, or input object types.

Dynamically changing query structure (Directives)

Directives @include and @skip allow you to include or skip entire query structures based on a boolean value:

query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    # also @skip(if: Boolean)
    friends @include(if: $withFriends) {
      name
    }
  }
}
{
  "episode": "JEDI",
  "withFriends": false
}

Sending mutations from a client

Creating or updating data is done via Mutations. We can mutate and query the new value of the field with one request.

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}
// the variables to pass in
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

In this example, the review variable we passed in is not a scalar. It’s an input object type.

If a mutation contains multiple fields, then they will be executed one after the other, not parallel as a query would. In other words, the first is guaranteed to finish before the second begins, ensuring that we don’t end up with a race condition.

Inline Fragments

Depending on a passed in query argument a query can return fields from different types. Example: If $ep is “JEDI” then the field name of type Droid will be returned, otherwise the field height of type Human is returned. We specify this using Inline Fragments:

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}

Meta fields

In your client you can check the returned __typename meta field to determine which type was returned:

{
  search(text: "an") {
    __typename
    ... on Human {
      name
    }
    ... on Droid {
      name
    }
    ... on Starship {
      name
    }
  }
}
{
  "data": {
    "search": [
      {
        "__typename": "Human",
        "name": "Han Solo"
      },
      {
        "__typename": "Human",
        "name": "Leia Organa"
      },
      {
        "__typename": "Starship",
        "name": "TIE Advanced x1"
      }
    ]
  }
}

This means that wherever we use the type Episode in our schema, we expect it to be exactly one of NEWHOPE, EMPIRE, or JEDI.

Lists and Non-Null

This following example means that the list itself can be null, but it can’t have any null members

myField: [String!]
myField: null // valid
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // error

The following example means that the list itself cannot be null, but it can contain null values:

myField: [String]!
myField: null // error
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // valid

Interfaces

This means that any type that implements Character needs to have these exact fields, with these arguments and return types

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

The following type implements the interface and additionally specifies own fields.

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

Union types

Union types are very similar to interfaces, but they don’t get to specify any common fields between the types. All of a union’s included types must be object types (not scalars, input types, etc.)

union SearchResult = Book | Author

type Book {
  title: String!
}


type Author {
  name: String!
}


type Query {
  search(contains: String): [SearchResult!]
}

GraphQL clients don’t know which object type a field will return if the field’s return type is a union. To account for this, a query can include the subfields of multiple possible types.

query GetSearchResults {
  search(contains: "Shakespeare") {
    # Querying for __typename is almost always recommended,
    # but it's even more important when querying a field that
    # might return one of multiple types.
    __typename
    ... on Book {
      title
    }
    ... on Author {
      name
    }
  }
}

Subscriptions

Subscriptions are long-lasting GraphQL read operations using the WebSocket protocol instead of HTTP. Subscriptions can update their result whenever a particular server-side event occurs. Most commonly, updated results are pushed from the server to subscribing clients. For example, a chat application’s server might use a subscription to push newly received messages to all clients in a particular chat room.

To enable subscriptions, you must first swap to the apollo-server-express package.

More about it on the Apollo Server page.

About Author

Mathias Bothe To my job profile

I am Mathias, born 39 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 15 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.