Mar 6

GraphQL Types as a Superpower

Blog post's hero image

Typically, with regards to GraphQL, the basic assumption is that it “only fetches what you need.” While it is true that GraphQL is a language that defines data requirements so that we can relay them to each other, as one of Stellate's Software Engineers, I’m here to share that there’s more to this language than meets the eye.

Those of us in the field often hear the terms, “self-documenting”, “strongly typed”, and “introspective”. However, what do these mean and what advantages are there?

GraphQL contains 5 “design principles”. Here, I’d like to focus on the benefits of GraphQL’s Introspective nature and how Strong-Typing can give you an edge (no pun intended. Well, maybe - haha). By definition:

Introspective: GraphQL is introspective. A GraphQL service’s type system can be query-able by the GraphQL language itself. This unique aspect provides a powerful platform for building common tools and client software libraries.

Strong-typing: Every GraphQL service defines an application-specific type system. Requests are executed within the context of that type system. Given a GraphQL operation, tools can ensure that it is both syntactically correct and valid within that type system before execution, i.e. while developing our application, and the service can make certain guarantees about the shape and nature of the response.

This means that GraphQL allows:

  • Consumers to easily query the type-system of a schema.

  • The type-system or schema to be specific to the application.

  • Data guarantees to the consumer when performing operations against this schema.

Introspection

Let’s start out by looking at how we can get information about a GraphQL schema as a consumer of GraphQL. We can inspect the types of our schema by means of the introspectionQuery. This will return a JSON representation of all types in the schema and how they relate to each other. Isn’t it neat how we can use the GraphQL language to query the schema?

Having this JSON-representation of our schema we can validate our inputs before ever sending data to the server! We can validate fragments in selection-sets to verify whether or not they are allowed on a certain interface or union. Not only that, but we can generate client-side types for our results as well as input variables.

TypeScript

Now that we have this introspection data we can use tools like GraphQL Code Generator and gql.tada to analyze it. Below is a schema, let’s see if we can compare it to a query we are sending.

type Todo {
id: ID!
text: String!
completed: Boolean!
updatedAt: Date
}
type Query {
todo(id: ID!): Todo!
}

When our consumer has a query that looks like the following

Then we can compare the schema to the query and derive both the result- and input-types. If we’d apply that to the above example we’d end up with this:

type TodoQueryVariables = { id: string };
type TodoQueryResult = {
todo: {
id: string;
__typename: 'Todo'
}
}

This allows us to statically verify the inputs we are sending to the GraphQL server as well as have types for the result we are expecting to get back. These types can also help us catch nullable fields or fields that are potentially missing due to features like the Stream/Defer directives or because the field has errored out.

Nullability

The introspection does not exclusively tell us the shape of the data, it also tells us whether or not the data is nullable. In a GraphQL schema we’ll see this by means of the ! operator behind a type. The presence of the ! tells us that it’s not-nullable. Nullability allows us to make assumptions about our data inside of a normalized cache.

Let’s look at the following example. When we enter our application the first query could be:

query ($id: ID!) {
todo(id: $id) { id text completed }
}

Our introspection result tells us that all three of these fields won’t be null. Now if the next query does add a nullable field like our updatedAt field

query ($id: ID!) {
todo(id: $id) { id text completed updatedAt }
}

This tells us a crucial detail: we can immediately return the previous result (without updatedAt), removing the need for a loading spinner, while fetching the new result. We can assume the UI is prepared to handle a null updatedAt either way. Preparing the UI for a null result comes for free with our generated static-typing: in the case where its null it could mean that the Todo has never been updated.

Fragment matching

When we’re querying data on an abstract-type (interface or union) we’ll have to leverage fragments to tell the GraphQL server what fields we need for the specific types. A fragment is part of the syntax in GraphQL. Fragments allow us to specify a selection of fields for a certain type. For instance:

fragment TodoFields on Todo {
id
text
}

In declaring a fragment called TodoFields, we can utilize it on selections which, once queried, will produce the specified fields. Putting this into perspective, consider the following schema:

interface Car {
id: ID!
name: String!
}
type SportCar implements Car {
id: ID!
name: String!
speed: Int!
}
type FamilyCar implements Car {
id: ID!
name: String!
seats: Int!
}
type Query {
car(id: ID!): Car!
}

Now if we were to query this we can get back either a SportCar or a FamilyCar and the schema tells us that both of these need to have id and name defined as fields to classify as a Car interface, if they wouldn’t implement these fields the schema would fail to compile.

query {
car(id: "1") {
## We can query id and name because it's on the interface
id
name
## To query speed we need to add a condition
## that the type needs to be a SportCar
... on SportCar { speed }
__typename
}
}

If in the above case we get a FamilyCar from the API, it is unclear from the matching whether we’re dealing with a union or interface. With the schema we can tell that the field we’re querying returns an interface and we got an implementer of the interface back from the server. In this case it even has an additional field named seats which if we wanted to could start querying by adding a fragment on FamilyCar.

In our types this will also be reflected where we’ll have to narrow the type if we want to access specific properties of the SportCar type.

if (result.data.car.__typename === 'SportCar') {
// Now we can access result.data.car.speed
} else {
// Now we can't access result.data.car.speed
// as we are dealing with a FamilyCar
}

Conclusion

To me introspection is one of the most powerful design principles of GraphQL, it enables other tools/servers/... to interpret the data and its shape.

Any consumer, be that an application, a developer tool, or another server can use the introspection to derive information about the data served from a schema. Think about how tools like GraphiQL help with auto-completion, provide diagnostics and so much more.

Introspection being one standard mechanism that works the same for every single GraphQL API out there allows for amazing possibilities in developer experience tooling. What will you build based on introspection?