Learning Logo
Mobile Menu
Copy article linkShare Article on TwitterShare article on LinkedInShare article on FacebookShare article on Pinterest

GraphQL interfaces and unions - how to design GraphQL schema

David Mráz
David Mráz@davidm_ai
Author's Linkedin accountAuthor's Instagram accountAuthor's Twitter accountAuthor's Twitter account
Development

Introduction

This article is a part of free GraphQL Language course. To fully understand the content you need to be familiar with basic GraphQL concepts like SDL language, GraphQL document syntax or GraphQL object types and scalars. If you do not have this knowledge, you can take a look at our older articles.

In the GraphQL specification we are able to use two abstract types:

  • interfaces
  • unions

In this article we will go through the use cases for abstract types and how we can implement them in our GraphQL schema. Using abstract types can greatly improve your GraphQL schema design and simplify your queries and mutations.

What is an interface?

Let's first define what is interface in programming. There are various definitions for interfaces, but I found out that the most useful one for understanding GraphQL interfaces is as follows:

Interfaces are usually needed when we are looking to access certain group of objects that has to comply to properties defined by interface. With interface we are basically abstracting group of types. By this abstraction we can reason about them as one entity.

Interfaces in GraphQL

Let's first define our model schema in SDL, so that we can start to apply our interface example. We would like to cover the types Planet, Constellation, Galaxy* and Star. The types in SDL language can look for example like this:

type Galaxy {
id: ID!
createdAt: DateTime!
updatedAt: DateTime
name: String
planets: [Planet]
stars: [Star]
}
type Star {
id: ID!
createdAt: DateTime!
updatedAt: DateTime
name: String
class: StellarClassEnum
}
type Constellation {
id: ID!
createdAt: DateTime!
updatedAt: DateTime
name: String
galaxies: [Galaxy]
}
type Planet {
id: ID!
createdAt: DateTime!
updatedAt: DateTime
name: String
description: String
planetType: PlanetTypeEnum
}

As we mentioned when applying interface we are basically adding additional node above our types, so that we are able to access them as one entity. Let's say that we would like to access all the types from our schema as one single entity called Node. To visualize this let's take a look at the following graph

In GraphQL we need to define every interface with at least one field. For example, in Relay.js we use what is known as a Node interface. Node interface usually serves as an interface for every type, which has the id field. If you are not using Relay.js on your frontend, I would also recommend to use this pattern. In Relay.js the Node interface has just one field id and it looks like this

interface Node {
id: ID!
}

Implementing our custom Node interface

In our schema we have modified the common structure of Node interface to have the timestamps createdAt and updatedAt. Now let's take a look at how the Node interface is applied in our schema using SDL language. We first need to define the interface itself.

interface Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime
}

When defining an interface we need to specify the fields that all children of the interface need to have. Once the interface is defined we need to choose what types should implement this particular interface. The Node interface in our case is implemented by all mentioned types in our model schema example. The modified SDL schema then looks as follows

type Galaxy implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime
name: String
planets: [Planet]
stars: [Star]
}
type Star implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime
name: String
class: StellarClassEnum
}
type Constellation implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime
name: String
galaxies: [Galaxy]
}
type Planet implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime
name: String
description: String
planetType: PlanetTypeEnum
}

We can see that every type implemented by the interface also has the same fields in common id, createdAt and updatedAt. These fields need to be also explicitly written for the type. If that is not the case, the GraphQL schema will be invalid. We are not constrained by using just Node interface. We can abstract different groups of types in our schema, such as multiple types of users (usually known as an Actor interface). The greatest advantage to this is it allows us to access a group of types in one single logical entity. This leads to a much cleaner schema design, as well as reducing the complexity of frontend.

Reducing the schema complexity

Now let's look at an example in which we reduce the complexity of queries and mutations. In our schema we would like to fetch Constellation, Planet, Stars or Galaxy based on the id. One approach is to add the query with the argument id for each different type for example like this.

type Query {
star(id: ID!): Star
planet(id: ID!): Planet
galaxy(id: ID!): Galaxy
constellation(id: ID!): Constellation
}

As you can see the approach is not scalable. The better way is to add just one query, node, which will return the Node interface. This query can be applied to retrieve any type which implements the Node interface. In most schemas you should implement the Node interface in every object type with id as the id is used as a global identifier. We can define the node query in SDL as follows:

type Query {
node(
"""The ID of the object"""
id: ID!
): Node
}

We would like to fetch just one Planet with "node query". To be able to do that we first need to fetch the id for some Planet, which we can use as an argument for the “node query”. We can obtain ids of the Planets by executing for example this query.

query getPlanets {
planets {
nodes {
id
name
}
}
}

After executing this query we receive the list of planets objects with the id and name for each planet.

{
"data": {
"planets": {
"nodes": [
{
"id": "UGxhbmV0OjUwMTA1MTc5MzQ2Nzg1NDg2MQ==",
"name": "Mars"
},
{
"id": "UGxhbmV0OjUwMTA1MTc5MzQ2Nzg1NDg2MA==",
"name": "Earth"
}
]
}
}
}

We can then copy one of the ids and use the Node interface to fetch more detailed info about the planet with this query

query getPlanet($id: ID!) {
node(id: $id) {
... on Planet {
id
name
description
createdAt
updatedAt
}
}
}

with the following variables

{
"id": "UGxhbmV0OjUwMTA1MTc5MzQ2Nzg1NDg2MQ=="
}

Then you will receive the full detail of the planet

{
"data": {
"node" {
"id": "UGxhbmV0OjUwMTA1MTc5MzQ2Nzg1NDg2MQ==",
"name": "Mars",
"description": "Mars is the fourth planet from the Sun and the second-smallest planet in the Solar System after Mercury. In English, Mars carries a name of the Roman god of war, and is often referred to as the Red Planet.",
"createdAt": "2018-10-14T15:20:56.668Z",
"updatedAt": null
}
}
}

This is the example that will be used a lot whenever you want to make a frontend for GraphQL-driven software. We can have for example a table of planets, but we do not want to fetch the whole planet instantly as it would be too expensive performance-wise and we will not use all the planet fields. We can render the name of the planets and, once we need more details, planet details with the node query. In our GraphQL document above we have introduced a new feature in GraphQL called fragments. For more info on fragments you can take a look at our article. Fragments help us tell the GraphQL server which fields we would like to retrieve for each type implemented by the interface. Now let’s check out the second abstract type called union.

GraphQL union

In the section above we went through the interface type. But what if we want to apply the similar abstraction to types that do not have any fields in common? For the Node interface example we assumed that every type which implements an interface contains the fields id, createdAt and updatedAt. But let's consider two types which has no fields. We can think about these types as a special kind of interface, where “interface” has no field and therefore does not enforce any properties. The union type is great if you do not want to constrain yourself to validating children types. We just want to represent multiple types that do not need to have any fields in common. Let's say we want to implement a query for searching within our schema. However, there is the constraint that we can only search for Planet and Galaxy. We want to abstract these two types and access them as one "searchable" entity without enforcing any other properties on them. This is a great use case for using Union type. Let's take a look at how we would implement this search query in the SDL language:

union Searchable = Planet | Galaxy

To search with the GraphQL document we again need to use inline fragments to determine on which type we would like to apply our selection set, just as we do for interfaces.

Summary

Interfaces and unions are the only available abstract types in the GraphQL specification. You can leverage them to reduce complexity of your schema, reduce the number of queries and mutations, and describe your data in a much more precise way. You might also ask yourself why we need two different abstract types. Why can’t the Union type just be an interface without any enforcing properties? Other people have asked this question as well. You can learn more about this topic in the following link. Please also note that all abstract types in GraphQL can be implemented only by output types. I hope that this article was useful to you and helps you to understand abstract types in GraphQL better.

Ready to take next step?

Unlock your potential and master the art of development and design by joining our Classes today.

Don't miss out on the opportunity to enhance your skills and create a bright future in the digital world like thousands of others.