The GraphQL Spec, Simplified

Loren Sands-Ramshaw
The GraphQL Guide
Published in
8 min readJun 3, 2021

The spec explained in plain language 🤓

Excerpt from the spec: the definition of a document

This is the first in a weekly series of posts explaining the essential parts of the GraphQL specification. To receive the rest of the series, hit the follow button ☺️

Part 1: Query language

Most people who use GraphQL haven’t read the spec, often because it sounds or looks intimidating. In this post, we’ll go over the essentials of the query language section of the spec, which has these parts:

  • Operations
  • Document
  • Selection sets
  • Fields
  • Arguments
  • Variables
  • Field aliases
  • Fragments
  • Directives
  • Mutations
  • Subscriptions

The second post will cover the type system (which we use on the server to build our schema), and the third will cover validation and execution.

Operations

GraphQL is a specification for communicating with the server. We communicate with it — asking for data and telling it to do things — by sending operations. There are three types of operations:

  • query fetches data
  • mutation changes and fetches data
  • subscription tells the server to send data whenever a certain event occurs

Operations can have names, like AllTheStars in this query operation:

query AllTheStars {
githubStars
}

Document

Similar to how a JSON file or string is called a “JSON document”, a GraphQL file or string is called a GraphQL document. There are two types of GraphQL documents — executable documents and schema documents. In the first part of the series, we’ll mainly be discussing executable (query) documents, and we’ll cover schema documents in the next post. An executable document is a list of one or more operations or fragments (which we’ll get to later). This document has a single query operation:

query {
githubStars
}

Our operation has a single root field, githubStars. In this type of document — a single query operation without variables or directives — we can omit query, so the above document is equivalent to:

{
githubStars
}

A more complex document could be:

query StarsAndChapter {
githubStars
chapter(id: 0) {
title
}
}
mutation ViewedSectionOne {
viewedSection(id: "0-1") {
...sectionData
}
}
mutation ViewedSectionTwo {
viewedSection(id: “0–2”) {
...sectionData
}
}
fragment sectionData on Section {
id
title
}
subscription StarsSubscription {
githubStars
}

It has all the operation types as well as a fragment. Note that when we have more than one operation, we need to give each a name — in this case, StarsAndChapter, ViewedSection*, and StarsSubscription.

Selection sets

The content between a pair of curly braces is called a selection set—the list of data fields we’re requesting. For instance, the selection set in the above StarsAndChapter query lists the githubStars and chapter fields:

{
githubStars
chapter(id: 0) {
title
}
}

And chapter has its own selection set: { title }.

Fields

A field is a piece of information that can be requested in a selection set. In the above query, githubStars, chapter, and title are all fields. The first two are top-level fields (in the outer selection set, at the first level of indentation), and they’re called root query fields. Similarly, viewedSection in the document below is a root mutation field:

mutation ViewedSectionTwo {
viewedSection(id: "0-2") {
...sectionData
}
}

Arguments

On the server, a field is like a function that returns a value. Fields can have arguments: named values that are provided to the field function and change how it behaves. In this example, the user field has an id argument, and profilePic has width and heightarguments:

{
user(id: 1) {
name
profilePic(width: 100, height: 50)
}
}

Arguments can appear in any order.

Variables

We often don’t know argument values until our code is being run — for instance, we won’t always want to query for user #1. The user ID we want will depend on which profile page we’re displaying. While we could edit the document at runtime (like '{ user(id: ' + currentPageUserId + ') { name } }'), we recommend instead using static strings and variables. Variables are declared in the document:

query UserName($id: Int!) {
user(id: $id) {
name
}
}

And their values are provided separately, like this:

{
"id": 2
}

After the operation name, we declare ($id: Int!) —the name of the variable with a $ and the type of the variable. Int is an integer and ! means that it’s required (“non-null”). Then, we use the variable name $id in an argument in place of the value: user(id: $id) instead of user(id: 2). Finally, we send a JSON object with variable values along with the query document.

We can also give variables default values, for instance:

query UserName($id: Int = 1) {
user(id: $id) {
name
}
}

If $id isn’t provided, 1 will be used.

Field aliases

We can give a field an alias to change its name in the response object. In this query, we want to select profilePic twice, so we give the second instance an alias:

{
user(id: 1) {
id
name
profilePic(width: 400)
thumbnail: profilePic(width: 50)
}
}

The response object will be:

{
"user": {
"id": 1,
"name": "John Resig",
"profilePic": "https://cdn.site.io/john-400.jpg",
"thumbnail": "https://cdn.site.io/john-50.jpg"
}
}

Fragments

  • Named fragments
  • Type conditions
  • Inline fragments

Named fragments

Fragments group together fields for reuse. Instead of this:

{
user(id: 1) {
friends {
id
name
profilePic
}
mutualFriends {
id
name
profilePic
}
}
}

we can combine fields with a fragment that we name userFields:

query {
user(id: 1) {
friends {
...userFields
}
mutualFriends {
...userFields
}
}
}
fragment userFields on User {
id
name
profilePic
}

Type conditions

Fragments are defined on a type. The type can be an object, interface, or union. When we’re selecting fields from an interface or union, we can conditionally select certain fields based on which object type the result winds up being. We do this with fragments. For instance, if the user field had type User , and User was an interface implemented by ActiveUser and SuspendedUser, then our query could be:

query {
user(id: 1) {
id
name
...activeFields
...suspendedFields
}
}
fragment activeFields on ActiveUser {
profilePic
isOnline
}
fragment suspendedFields on SuspendedUser {
suspensionReason
reactivateOn
}

Then, the server will use the fragment that fits the type returned. If an ActiveUser object is returned for user 1, the client will receive the profilePic and isOnline fields.

Inline fragments

Inline fragments don’t have a name and are defined inline — inside the selection set, like this:

query {
user(id: 1) {
id
name
... on ActiveUser {
profilePic
isOnline
}
... on SuspendedUser {
suspensionReason
reactivateOn
}
}
}

Directives

Directives can be added after various parts of a document to change how that part is validated or executed by the server. They begin with an @ symbol and can have arguments. There are three included directives, @skip, @include, and@deprecated, and servers can define custom directives.

@skip

@skip(if: Boolean!) (spec) is applied to a field or fragment spread. The server will omit the field/spread from the response when the if argument is true. Sending this document:

query UserDeets($id: Int!, $textOnly: Boolean!) {
user(id: $id) {
id
name
profilePic @skip(if: $textOnly)
}
}

with these variables:

{
"id": 1,
"textOnly": true
}

to the server would result in this response:

{
"data": {
"user": {
"id": 1,
"name": "John Resig"
}
}
}

While the spec doesn’t dictate using JSON to format responses, it is the most common format.

@include

@include(if: Boolean!) (spec) is the opposite of @skip, only including the field/spread in the response when theif argument is true. Sending this document:

query UserDeets($id: Int!, $adminMode: Boolean!) {
user(id: $id) {
id
name
email @include(if: $adminMode)
groups @include(if: $adminMode)
}
}

with these variables:

{
"id": 1,
"adminMode": false
}

would result in this response:

{
"data": {
"user": {
"id": 1,
"name": "John Resig"
}
}
}

@deprecated

Unlike @skip and @include, which are used in executable documents, @deprecated is used in schema documents. It is placed after a field definition or enum value to communicate that the field/value is deprecated and why — it has an optional reason String argument that defaults to “No longer supported.”

type User {
id: Int!
name: String
fullName: String @deprecated("Use `name` instead")
}

Mutations

Mutations, unlike queries, have side effects — i.e., alter data. The REST equivalent of a query is a GET request, whereas the equivalent of a mutation is a POST, PUT, DELETE, or PATCH. Often, when the client sends a mutation, it selects the data that will be altered so that it can update the client-side state.

mutation {
upvotePost(id: 1) {
id
upvotes
}
}

In this example, the upvotes field will change, so the client selects it (i.e., includes it in the selection set).

While not enforced by the specification, the intention and convention is that only root mutation fields like upvotePost alter data — not subfields like id or upvotes , and not Query or Subscription fields.

We can include multiple root fields in a mutation, but they are executed in series, not in parallel. (All fields in a mutation below the top level and all query fields are executed in parallel.) This way, assuming the code resolving the first root mutation field waits for all of the side effects to complete before returning, we can trust that the second root mutation field is operating on the altered data. If the client wants the root fields to be executed in parallel, they can be sent in separate operations.

While technically “mutation” is an operation type, the root mutation fields are often called “mutations.”

Subscriptions

Subscriptions are long-lived requests in which the server sends the client data from events as they happen. The manner in which the data is sent is not specified, but the most common implementation is WebSockets, and other implementations include HTTP long polling, server-sent events (supported by all browsers except for IE 11), and webhooks (when the client is another publicly addressable server).

The client initiates a subscription with:

subscription {
reviewCreated {
id
text
createdAt
author {
name
photo
}
}
}

As with mutations, we call the subscription operation’s root field the “subscription,” and its selection set is the data that the server sends the client on each event. In this example, the event is the creation of a review. So whenever a new review is created, the server sends the client data like this:

{
"data": {
"reviewCreated": {
"id": 1,
"text": "Now that’s a downtown job!",
"createdAt": 1548448555245,
"author": {
"name": "Loren",
"photo": "https://avatars2.githubusercontent.com/u/251288"
}
}
}
}

Summary

To recap the GraphQL query language, we can send one or more operations in a GraphQL document. Each operation has a (possibly nested) selection set, which is a set of fields, each of which may have arguments. We can also:

  • Declare variables after the operation name.
  • Alias fields to give them different names in the response object.
  • Create named fragments to reuse fields and add type conditions to conditionally select fields from interfaces and unions.
  • Add directives to modify how the server handles a part of a document.
  • Use mutations to alter data.
  • Use subscriptions to receive events from the server.

Next week we’ll go through the GraphQL type system, and the following week we’ll go over validation and execution of GraphQL documents. To get notified, you can follow on Medium (button below) or on Twitter.

This post is an excerpt from our book, The GraphQL Guide. At 885 pages, it’s the complete reference text for GraphQL, with a beginner introduction as well as advanced client and server topics. Grab a copy at graphql.guide 🤗 (and use your company’s educational budget to get reimbursed 🙌)

--

--