You are viewing the preview version of this book
Click here for the full version.

Queries and Mutations

Queries in GraphQL are based on two things: an entry point and traversals. The entry point is the initial object (or objects) that are coming from a Query or a Mutation type. This provides a "foothold" in the objects graph.

Then the query can define what other fields and objects it needs in the response. This makes GraphQL queries powerful: the client can define what it needs and it gets exactly that.

For example, a client might want to show the social graph for a user, so it fetches the friends:

query {
  user(id: "user1") {
    name
    friends {
      name
    }
  }
}

A different client might want to show an admin panel and show all the users with their email addresses and permissions:

query {
  allUsers {
    name
    email
    permissions {
      name
    }
  }
}

Both queries above use the same object graph, but they get different data. This gives GraphQL queries a lot of flexibility and if used well provides a faster user experience.

Note

It's all too easy to query everything and then select what is needed on the client-side. But this does not utilize the flexibility of the queries and goes against the best practices for GraphQL.

But queries are a lot more powerful than just selecting what fields the client needs. GraphQL provides a lot of features to fine-tune their behavior. In this chapter we'll look into the most useful ones and how to use them.

Fields

(Official docs)

What gives the most versatility for GraphQL queries is the ability to select the fields they need in the response. A type might have many fields, but if the client needs just a few of them, then it consumes less resources to omit the unneeded ones.

For example, a User object has a username, a bio, and an email address:

type User {
  username: String!
  email: String
  bio: String
}

type Query {
  user(username: String!): User
}

If the client does not need the email address, it can choose not to ask for that:

# query
query MyQuery {
  user(username: "user1") {
    username
    bio
  }
}

And the result:

{
  "data": {
    "user": {
      "username": "user1",
      "bio": "Lorem ipsum"
    }
  }
}
It's not just about the bandwidth

While most fields directly map to columns in a database, it is not always the case and the client should not assume it is "free" on the backend. For example, the User object in the database might only contain the username but not the email address. In a common scenario, returning the email means sending a call to Cognito (or a similar user directory) and extract the value from the response.

Because of this, clients should query only for data that they will actually use.

Fields can also be types defined in the schema. Getting a field that is not scalar is how a query can move from object to object in the graph. For example, showing the friends of a user means asking for the friend object and then their fields.

# schema
type User {
  username: String!
  email: String
  bio: String
  friends: [User!]!
}

type Query {
  user(username: String!): User
}
# query
query MyQuery {
  user(username: "user1") {
    username
    friends {
      username
    }
  }
}

And the result:

{
  "data": {
    "user": {
      "username": "user1",
      "friends": [
        {
          "username": "user2"
        },
        {
          "username": "user3"
        }
      ]
    }
  }
}

Arguments

(Official docs)

As we've seen in the Arguments chapter, fields of any type can define what arguments they need. These can be "top-level", such as a Query or a Mutation, but every field for every type can have arguments too.

A client query must provide all required arguments and it can fill any optional ones. When a query does not pass any arguments for a field the parentheses are missing.

For example, a query in the schema might define an optional argument and the client can choose whether to define that or not:

# schema
type Query {
  # No ! after String, so this is an optional argument
  allUsers(search: String): [User]
}

Both of these queries are valid:

query Query1 {
  allUsers {
    username
    email
  }
}

query Query2 {
  allUsers(search: "admin") {
    username
    email
  }
}

Arguments for fields work the same. Here, the username is required, while the after is optional:

# schema
type Ticket {
  text: String!
  # POSIX time
  created: Int!
}

type User {
  username: String!
  # the argument is optional
  tickets(after: Int): [Ticket]
}

type Query {
  # the argument is required
  user(username: String!): User
}

Defining arguments for a nested field works the same as for the query itself:

query Query1 {
  user(username: "user1") {
    username
    tickets(after: 1639907490) {
      text
      created
    }
  }
}

query Query2 {
  user(username: "user1") {
    username
    # no argument here
    tickets {
      text
      created
    }
  }
}

Variables

(Official docs)

As queries are text-based and arguments are usually user-supplied, for example a search field on a website, it's a bad idea to use string concatenation. Instead, GraphQL provides a way to separate the fixed part of the query from the variable part.

In this query, the variable part is the text argument of the search query:

query SearchQuery {
  search(text: "users") {
    text
    location
  }
}

With string concatenation it's all too easy to end up with something like this:

// don't do this

// search is the user-provided value
const graphQLQuery = `query SearchQuery {
  search(text: "${search}") {
    text
    location
  }
}`;

The problem here is that the value can contain characters that "break away" from the query argument. For example, if a user searches for something", then the resulting query is going to be:

query SearchQuery {
  search(text: "something"") {
    text
    location
  }
}

With the double ", the query is now invalid and the user will get an error.

To declare a variable for a query, give it a name and add it to the query object. Then whenever you want to define that value, use the variable name:

query SearchQuery($text: String) {
  search(text: $text) {
    text
    location
  }
}

Then pass the variable separately, which is dependent on the client library you are using.

For example, AppSync variables are in variables field as a JSON object:

Variables are defined separately for an AWS AppSync query
Note

Unlike SQL injection, string concatenation in a GraphQL query is not a security vulnerability. This is because it happens on the client-side, so a malicious actor can already send any query they wish.

Except when the dynamic part is defined by a client different than the one sending the request, such as a Lambda function that gets parameters from a HTTP request. In that case, not using variables can open security vulnerabilities.

Aliases

(Official docs)

There is more, but you've reached the end of this preview
Read this and all other chapters in full and get lifetime access to:
  • all future updates
  • full web-based access
  • PDF and Epub versions