Four Kitchens
Insights

GraphQL Leveler: Controlling shape in the query

4 Min. ReadDevelopment

I’ve heard it said (and even said it myself) that GraphQL allows clients to “control shape”. The idea is that the structure of the client’s query will determine the shape of the response data. Unfortunately, this is simply not true. The reality is, although GraphQL provides many powerful capabilities to clients, the ability to define the response shape is not one of them… or at least it wasn’t until now!

A common misconception

I can see why this has been a common misconception. GraphQL queries do resemble the structure of response data and do define which fields will be returned. There are even some shape-shifty-kinds-of-things you can do like rename and duplicate properties. That feels an awful lot like controlling the shape! But it’s only an illusion — an incredibly intuitive query interface, but not shape control.

Try accessing a nested property at the root of your request, or try moving a top-level property into a nested object. These should be very simple operations if the client can control shape in the query, right? But it’s not possible in a GraphQL query.

For a concrete example, let’s say you are trying to access a deeply nested property like the “id” property from the object below:

{
  "relationships": {
    "episode": {
      "data": {
        "id": "123"
      }
    }
  }
}

And let’s say you want it at the root of the response with the property name “episodeID”. You can rename the property to “episodeID” no problem, but there’s no way to move the “id” property to the root of the response. The query and response would have to end up looking something like this:

{
  relationships {
    episode {
      data {
        episodeID: id
      }
    }
  }
}

{
  “data”: {
    "relationships": {
      "episode": {
        "data": {
          "episodeID": "123"
        }
      }
    }
  }
}

Ultimately, the server’s type definitions control the shape of the response to the client. But what if there was a way for the server’s type definitions to expose this shape-defining functionality?

Introducing: GraphQL Leveler

Enter GraphQL Leveler! GraphQL Leveler is a library for GraphQL servers which allows them to provide a great deal of flexibility to their clients when it comes to response shape. It does this by exposing two simple, but extremely powerful, fields on objects in their type definitions: _get and _root. Let’s take a look from a client’s perspective at the possibilities these two fields open up.

The _get Field

Let’s say our GraphQL server defines an object which resolves with the same shape as before:

{
  "relationships": {
    "episode": {
      "data": {
        "id": "123"
      }
    }
  }
}

But this time, the server is using GraphQL Leveler to define its objects. With the _get field, clients can query for the id property like this:

{
  episodeId: _get(path: “relationships.episode.data.id”)
}

{
  “data”: {
    “episodeId”: “123”
  }
}

Neat! We’ve “leveled” a deeply nested object, transforming it to a flat shape.

The _get field also offers additional flexibility on the client with functionality like defining default values and allowing/disallowing undefined values. Keep in mind, however, it is not without limitations.

The _root field

While the _get field essentially flattens objects, the _root field allows you to define your own object structure independently from the shape defined by the server. It does this by allowing access to the root of the object where it would normally be out of scope. Use it in combination with aliases and you can create completely arbitrary shapes on the client. For example, suppose our server defines an object which resolves to a shape like this:

{
  "episodeNumber": 42,
  "seasonNumber": 1
}

And let’s say we want a response shape resembling something like JSON API where we have an “episode” and “season” object inside a “relationships” object. Normally this wouldn’t be possible, but since our server is using GraphQL Leveler, we can send a query with the _root field like this:

{
  relationships: _root {
    episode: _root {
      data: _root {
        attributes: _root {
          number: episodeNumber
        }
      }
    }
    season: _root {
      data: _root {
        attributes: _root {
          number: seasonNumber
        }
      }
    }
  }
}

{
  "data": {
    "relationships": {
      "episode": {
        “data”: {
          “attributes”: {
            “number”: 42
          }
        }
      },
      "season": {
        "data": {
          “attributes”: {
            “number”: 1
          }
        }
      }
    }
  }
}

Cool! Now you can flatten and expand shape with extreme flexibility on the client in GraphQL!

Get started

Adding this shape-shifting functionality for API clients is easy! GraphQL Leveler exports a LevelerObjectType which is a drop-in replacement for GraphQLObjectType. Simply add graphql-leveler to your dependencies, require LevelerObjectType, and use it instead of GraphQLObjectType. Done!

const {
  GraphQLString,
  GraphQLInt,
  GraphQLObjectType,
} = require('graphql');
const {
  LevelerObjectType,
} = require('graphql-leveler');

// This would have been GraphQLObjectType before.
const PersonType = new LevelerObjectType({
  name: 'person',
  fields: () => ({
    attributes: {
      // This would have been GraphQLObjectType before.
      type: new LevelerObjectType({
        name: 'personAttributes',
        fields: () => ({
          name: { type: GraphQLString },
          height: { type: GraphQLInt },
          eye_color: { type: GraphQLString },
        }),
      }),
    },
  }),
});

Keep the misconception!

Now our clients have a great deal of control over shape in the query. No more misconception! You can absolutely control shape in the client with GraphQL (and GraphQL Leveler)!