Apr 20, 2022

How GraphQL's Introspective Design Principle Gives the Upperhand to Client Types

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 queryable 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. at development time, 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 data and verify whether the data in our client-side cache is accurate.

Typescript

Now that we have this introspection data we can use tools like GraphQL Code Generator to analyze it. Below is a schema.  See if you 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

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

Then we can compare the introspection result 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:

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

This tool 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 {
todo(id: "1") {
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

query {
todo(id: "1") {
id
text
completed
updatedAt
}
}

This tells us a crucial detail: we can return the previous result (without updatedAt) while fetching the new result as the UI is prepared to handle a nullish updatedAt either way. Preparing the UI for a nullish result comes for free with our generated types: in this case 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 that, once queried, will produce the specified field. 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 to classify as a Car interface.

query {
car(id: "1") {
id
name
... on SportCar {
speed
}
__typename
}
}

If in the above case we get a FamilyCar from the API, it is unclear about the matching: whether we’re dealing with a union or interface. With the schema we can easily tell that we got a different implementor back from the server and that it even has an additional field: seats.

Conclusion

To me introspection is one of the most powerful design principles of GraphQL, we enable 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 or how Stellate lists out the possible types and fields for cache rules.

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?