So, you’re curious about this GraphQL thing - or maybe just checking whether I know what I’m talking about. Either way, welcome! This won’t be your typical tutorial - you can find plenty of those elsewhere.

Why am I writing this, you ask? Because when I first learned about GraphQL, the resources I found didn’t clear up my misconceptions. In some cases, they even reinforced them. I wasted too much time looking for, expecting, and worrying about things that didn’t actually exist. I’d like to save you from making the same mistakes I did.

To get the most out of this, start with an open mind. Assume that anything you think you know about GraphQL might not be true - yes, even if you were one of its creators. To help with that, I recommend reading my earlier post on what GraphQL isn’t. I wish I’d had something like that when I started; it would have saved me a lot of time.

Now, here’s where things get a little unconventional. To make this easier, I won’t actually be writing about GraphQL until the end. Instead, we’re going to design something of our own: JSONTL, a JSON Template Language. Trust me, this detour will be worth it.

Starting Point

You’re probably familiar with at least one template language - HTML template languages are a common example. Now, imagine designing something similar, but for JSON.

Why would we want to do that? Well, let’s say we’re tired of writing tedious code (like the example on the left, below) just to generate JSON (like the example on the right), all without relying on serialization, marshalling, or object-mapping frameworks.

The following JSON snippet illustrates our end goal: retrieving details about a specific item available for sale.

Template Pseudo-Code Target JSON
it = getItem(id: "a25e2223-faec-48e0-b54c-d9f48bd4c5d9");
data = {
  item: {
    upc: it.getUPC(),
    name: it.getName(locale: ENGLISH_CANADIAN),
    price: it.getPrice(),
    bulkPrice: it.getPrice(currency: CAD, count: 10),
    stock: it.getStock(near: "M4C 1M2").map(storeStock -> // list
      {
        storeName: storeStock.getStoreName(),
        count: storeStock.getCount()
      }
    )
  }
}
// 
data: {
  item: {
    upc: 72527273070,
    name: "The Enchanted Iciql",
    price: 19.99,
    bulkPrice: 179.91,
    stock: [
      {
        storeName: "Whimsiql Books",
        count: 24
      }, 
    ]
  }
}

You should be able to follow the template pseudo-code - it’s designed to create a data representation structure that is directly and seamlessly reflected in the output. If you’re viewing this on a wide enough screen, the lines of the template and the corresponding output should align for easier comparison.

In this example, I’m using constructs like named and optional arguments, enums (e.g., ENGLISH_CANADIAN and CAD), and lists (ordered collections, arrays). I’m also leveraging lambda expressions to map each store’s stock data into a structure that contains only the relevant details for the target JSON.

For clarity, I’ve left out quotation marks around key/field names in the target JSON - feel free to imagine them back in if needed.

That said, there’s quite a bit of boilerplate here. The left side doesn’t resemble the right side much at all, and there’s a lot of duplication. The stock details mapping, in particular, looks cumbersome. Let’s improve that.

Mapping Sugar

We’re going to change our language and add “syntactic sugar” as follows:

When a function call, such as getStock(…) is followed by a {…} block we’ll treat that as a .map( x -> {…}) construct. In other words

We can remove it. and storeStock. before getters as we’re going to default to the object returned by the function invoked just before the mapping block.

While we’re there, we’re going to require that construct for anything yielding non-primitive values, such as structures or objects. In this example, all other function calls return primitives.

If this construct is applied to a single-valued result, we just transform that value. In the example, it is applied to a list, so we apply it to each element.

In other words, we want to express the following:

a(...).map(x -> {
  p: x.b(...),
  q: x.c(...),
  ...
})

More concisely as follows:

a(...) {
  p: b(...)
  q: c(...)
}

… regardless of whether a(…) returns a single value or a collection (list, array, …). That cleans up our template code to the following:

Template Pseudo-Code Target JSON
data = {
  item: getItem(id: "a25e2223-faec-48e0-b54c-d9f48bd4c5d9") {
    upc: getUPC(),
    name: getName(locale: ENGLISH_CANADIAN),
    price: getPrice(),
    bulkPrice: getPrice(currency: CAD, count: 10),
    stock: getStock(near: "M4C 1M2") // list
      {
        storeName: getStoreName(),
        count: getCount()
      }

  }
}
data: {
  item: {
    upc: 72527273070,
    name: "The Enchanted Iciql",
    price: 19.99,
    bulkPrice: 179.91,
    stock: [
      {
        storeName: "Whimsiql Books",
        count: 24
      }, 
    ]
  }
}

Getter Name Magic

Some languages provide convenient features for accessing “properties” backed by functions or methods without explicitly referencing the function/method names. If a property is read, it automatically calls a getter; if assigned, it uses a setter.

We’re only interested in getters - not setters, not now, not ever. Since setters are irrelevant to our use case, we’ll simply ignore them.

In any case, we’re going to borrow this feature. If it helps, think of it as automatically prefixing function names with “get”, saving us from typing those three extra letters or worrying about capitalization.

That brings us to:

Template Pseudo-Code Target JSON
data = {
  item: item(id: "a25e2223-faec-48e0-b54c-d9f48bd4c5d9") {
    upc: upc(),
    name: name(locale: ENGLISH_CANADIAN),
    price: price(),
    bulkPrice: price(currency: CAD, count: 10),
    stock: stock(near: "M4C 1M2") // list
      {
        storeName: storeName(),
        count: count()
      }

  }
}
data: {
  item: {
    upc: 72527273070,
    name: "The Enchanted Iciql",
    price: 19.99,
    bulkPrice: 179.91,
    stock: [
      {
        storeName: "Whimsiql Books",
        count: 24
      }, 
    ]
  }
}

Remove Empty Argument Lists

Why bother with () at all?

Since we’ve decided that we don’t need to reference or pass functions directly, let’s simplify things and drop the () from upc(), price(), storeName(), and count():

Template Pseudo-Code Target JSON
data = {
  item: item(id: "a25e2223-faec-48e0-b54c-d9f48bd4c5d9") {
    upc: upc,
    name: name(locale: ENGLISH_CANADIAN),
    price: price,
    bulkPrice: price(currency: CAD, count: 10),
    stock: stock(near: "M4C 1M2") // list
      {
        storeName: storeName,
        count: count
      }

  }
}
data: {
  item: {
    upc: 72527273070,
    name: "The Enchanted Iciql",
    price: 19.99,
    bulkPrice: 179.91,
    stock: [
      {
        storeName: "Whimsiql Books",
        count: 24
      }, 
    ]
  }
}

Silence the echoes

We have a lot of something: something there:

item: item
upc: upc
price: price
stock: stock
storeName: storeName
count: count

The first (or leftmost) value is the name of the target JSON field - let’s call it an “alias”. The second value specifies which function to call to retrieve the corresponding value. Now, let’s make aliases optional. If omitted, we’ll simply default them. In our example, only one case doesn’t benefit from this shortcut: “bulkPrice” - it has to remain as is.

Template Pseudo-Code Target JSON
data = {
  item(id: "a25e2223-faec-48e0-b54c-d9f48bd4c5d9") {
    upc,
    name(locale: ENGLISH_CANADIAN),
    price,
    bulkPrice: price(currency: CAD, count: 10),
    stock(near: "M4C 1M2") // list
      {
        storeName,
        count
      }

  }
}
data: {
  item: {
    upc: 72527273070,
    name: "The Enchanted Iciql",
    price: 19.99,
    bulkPrice: 179.91,
    stock: [
      {
        storeName: "Whimsiql Books",
        count: 24
      }, 
    ]
  }
}

Shorter comments

Not that it matters much, but say we dislike the use of // for end-of-line comments and want to shorten it to #. Let’s do that:

Template Pseudo-Code Target JSON
data = {
  item(id: "a25e2223-faec-48e0-b54c-d9f48bd4c5d9") {
    upc,
    name(locale: ENGLISH_CANADIAN),
    price,
    bulkPrice: price(currency: CAD, count: 10),
    stock(near: "M4C 1M2") # list
      {
        storeName,
        count
      }

  }
}
data: {
  item: {
    upc: 72527273070,
    name: "The Enchanted Iciql",
    price: 19.99,
    bulkPrice: 179.91,
    stock: [
      {
        storeName: "Whimsiql Books",
        count: 24
      }, 
    ]
  }
}

Remove the envelope

Our JSON template will produce JSON, not JavaScript or some other code. Its output is only one value, structure. That data = at the beginning, is out of place and can cause trouble. I mean, what does that = even do? We’ll simplify things and always assume that the output is going to be named data. We get to:

Template Pseudo-Code Target JSON
{
  item(id: "a25e2223-faec-48e0-b54c-d9f48bd4c5d9") {
    upc,
    name(locale: ENGLISH_CANADIAN),
    price,
    bulkPrice: price(currency: CAD, count: 10),
    stock(near: "M4C 1M2") # list
      {
        storeName,
        count
      }

  }
}
data: {
  item: {
    upc: 72527273070,
    name: "The Enchanted Iciql",
    price: 19.99,
    bulkPrice: 179.91,
    stock: [
      {
        storeName: "Whimsiql Books",
        count: 24
      }, 
    ]
  }
}

The Charade is Over

How does that template look to you? Can you see yourself making use of it? Well, surprise! We just reinvented GraphQL. That last version? Yep, it’s GraphQL. We’ve covered the basics, and while there are a few more advanced features, they follow the same principles. For instance, commas and most whitespace are optional; they’re treated simply as separators. Here’s an example of valid GraphQL:

Template Pseudo-Code Target JSON
{
  item(id: "a25e2223-faec-48e0-b54c-d9f48bd4c5d9") {
    upc,,,,  ,,, ,, # Yes, commas are just whitespace
    name(locale: ENGLISH_CANADIAN)
    price
    bulkPrice: price(currency: CAD, count: 10)
    stock(near: "M4C 1M2") {

      storeName
      count
    }

  }
}
data: {
  item: {
    upc: 72527273070,
    name: "The Enchanted Iciql",
    price: 19.99,
    bulkPrice: 179.91,
    stock: [
      {
        storeName: "Whimsiql Books",
        count: 24
      }, 
    ]
  }
}

… as is this:

{item(id: "a25e2223-faec-48e0-b54c-d9f48bd4c5d9"){upc name(locale:ENGLISH_CANADIAN) 
price bulkPrice: price(currency:CAD,count:10) stock(near:"M4C 1M2"){storeName,count}}}

So, what exactly is GraphQL? You could think of it as:

  • A JSON template language that API clients use in requests to servers.

  • A functional or RPC-style API with a composable twist: it allows easy nesting of any number of follow-up calls on the output of the preceding (outer, containing, parent) one.

Where the query?

I admit, I cheated a little. That was indeed valid GraphQL, but it took advantage of a convenience feature that lets us omit the query keyword at the beginning. A more complete example would look like this:

query { # Notice the keyword here
  item(id: "a25e2223-faec-48e0-b54c-d9f48bd4c5d9") {
    upc
    name(locale: ENGLISH_CANADIAN)
    price
    bulkPrice: price(currency: CAD, count: 10)
    stock(near: "M4C 1M2") {
      storeName
      count
    }
  }
}

Do we have to construct queries dynamically?

Yes, constructing the query each time a different argument needs to be passed isn’t ideal. GraphQL does support parameterization, but it refers to the concept as “variables”. I won’t dive too deep into it, but here’s a quick glimpse of what it looks like:

query CanadaEnglishItemInquiry($itemId: UUID! $quantity: Int! $near: PostalCode) {
  item(id: $itemId) {
    upc
    name(locale: ENGLISH_CANADIAN)
    price
    bulkPrice: price(currency: CAD, count: $quantity)
    stock(near: $near) {
      storeName
      count
    }
  }
}

How does it know what’s available?

Ah, great question! 😉 Why, you ask? Well, you wouldn’t want all your functions or methods to be exposed, right? Even if you did, GraphQL wouldn’t know how to handle it on its own. Something needs to tell it what to expose - and that’s where the other GraphQL language comes in! What other language, you ask? That would be the “Schema Definition Language” (SDL) in GraphQL, used to define all the terms we’ve been talking about. Without a schema, GraphQL is completely empty. It has nothing. Most GraphQL frameworks I know of are schema-first to some extent (though they don’t have to be). They allow you to make connections between the GraphQL schema and the code you want to expose.

GraphQL SDL serves the same purpose for GraphQL that Swagger or OpenAPI does for other, more traditional HTTP-based APIs. By the way, the most common way to use GraphQL is over HTTP, and that can be represented using OpenAPI. The schema leverages GraphQL’s “type system”. In addition to a few primitive types (Boolean, Int, Float, String, and ID), it supports lists, nullability specifications, and custom-defined enum and complex types. Complex types are split into input-only (arguments) and output-only (responses) types. Currently, only output types support interfaces and unions.

To add a bit more detail: non-nullability is indicated by adding an exclamation mark to the type name, and lists are defined by enclosing the element types in square brackets. For example, [String!]! represents a non-null list of non-null strings, and [[Float!]!] represents a nullable list of non-nullable lists of non-nullable floats - a nullable two-dimensional matrix. The ID type is special: it indicates a value that should not be transformed or treated as anything other than an identifier. This is different from numbers, which can be summed, or strings, which can be truncated, parsed, etc.

Now I need to share that SDL?

Not exactly. This is the part where GraphQL actually defines something, which is why I lied to you when I said it was entirely empty. GraphQL includes a feature called “introspection”. This allows the client accessing the GraphQL API to ask the API about its schema. And, as you’d expect, the response is still in JSON format.

query {
  item(id: "a25e2223-faec-48e0-b54c-d9f48bd4c5d9") {
    __typename # Notice __
  }
  __schema { # Notice __
    types {
      name
      description
      fields {
        name
        ...
      }
    }
  }
}

… could produce something like:

data: {
  item: {
    __typename: "Book"
  }
  __schema: {
    types: [{
      name: "Book",
      description: "A type for book items",
      fields: [
        {
          name: "author"
          ...
        }, ...
      ]
    }, {}, {}, {}, ]
  }
}

Only read-only?

No, GraphQL isn’t read-only. Let’s think about this for a moment. Revisit those functions we called earlier, the “getters” from the initial examples. They can take input arguments and do a lot more than just retrieve values. What’s stopping you from having them do more? Did you consider that? Here’s the answer: nothing. That’s right. If you’re implementing a GraphQL server, there’s nothing to stop you from doing whatever you want and exposing it however you choose.

Let’s take a step back. Is our example request really 100% read-only? Are you sure nothing is being logged in the process? Could it be that the user’s profile is updated based on their interests? Or perhaps the level of interest for a product is being tracked? Both could be valuable for analytics.

So yes, you can create and expose functions that make changes. However, GraphQL provides a dedicated “space” for operations that can have “side effects”, nudging developers to register these non-read-only functions as “mutations”. The rest is up to your good judgment.

For example:

mutation {
  addToStock(store:  itemId:  quantity: 100) {
    count # how many do we have now?
  }
}

… may produce:

data: {
  addToStock: {
    count: 153
  }
}

Later, not now

If you don’t need (or want) the data to be returned right away, GraphQL also introduces the concept of “subscriptions”. These follow the same basic pattern but don’t assume the path the data will take, only that it won’t be sent back immediately. Here’s an example:

subscription {
  lowStockAlert(itemId: "…" alertQuantity: 5) {
    store { 
      storeName
    }
    count # how many do we have at the moment?
  }
}

Show me some HTTP

HTTP is the primary way to access GraphQL APIs. The HTTP GET method can be used for operations registered as queries, while POST can be used for any type of operation, including queries. Assuming your GraphQL API is exposed at /graphql, here’s an example of a valid request:

GET /graphql?query={item(id:1234){name,upc}}

The “query” we’ve been using in examples so far is simply passed as a raw text value in the URL query string, as the “query” parameter, and it’s escaped as needed. Variables can also be passed in as part of a “variables” JSON, but I won’t dive into those details here. With POST requests, the URL query string is not used. Instead, JSON can be used:

POST /graphql
Content-Type: application/json
… (more headers)

{"query":"item(id:1234){name,upc}}"}

Tomayto, Tomahto, Potayto, Potahto

The terms used in GraphQL make sense when viewed through the lens of the target JSON structure and the original intent behind its creation. However, these terms don’t quite capture the full potential that GraphQL has come to embody. To fully unlock its power, it’s helpful to think of these GraphQL concepts with a bit of creative reinterpretation.

For example, GraphQL uses the term “field” rather than “function”, even though it allows for input arguments and refers to “resolver functions”. To make things easier, I’ve put together a cheat sheet for these terms.

Is this everything?

Not quite. GraphQL has more to offer. I briefly touched on variables and didn’t delve into inline or named (reusable) fragments, which can help with applying common projections (“field selection”) throughout. There are also some interesting “curiosities” around field merging, execution order, subscription lifecycles, and directives. I only mentioned SDL in passing. These topics are more advanced and worthy of their own posts. If there’s interest and I get the chance, I’ll explore them further.

Next…

Continue to my post about making services collaborate via functional composition to see an advanced, powerful and highly popular use case for GraphQL.

Other posts dealing with similar topics