Doing More With GraphQL: Interfaces and Unions

Update: On April 16th 2021, Slash GraphQL was officially renamed Dgraph Cloud. All other information below still applies.

GraphQL’s declarative nature with its robust type system makes it a great choice for APIs. It allows designing system of services with enough flexibility that it can evolve over time, without requiring developers to refactor their design every time there is a change like a new feature or requirements to support new types of requests from clients. The specification is strict about how you structure and expose your data through types, unlike REST, while providing reliability and usability for the clients that they get an appropriate response upon making a request.

Apart from the scalar types, GraphQL offers two abstract types called interfaces and unions. Abstract types make your API easier to use and create room to grow your API in the future. Essentially it enables you to handle different states and multiple scenarios that might result from the client-side.

This article presents several examples that will give you a solid grasp of what you can do with these types, so you can use the capabilities they provide in your own projects.

Interfaces

Like most other typed languages like Typescript or Go, GraphQL also supports interfaces. An interface defines a set of fields that must be included by an object in order to implement that interface. This helps to maintain a clean schema where certain types have common fields. So these fields are always “queryable” no matter what type is returned.

For example, consider a website that sells books and home releases of films. If you send a query to their API, the result can be either a Book or a Film type. Ideally if it’s a Book you’d want the author’s name and if a Film then the director. But both of these types can have common fields such as a name, id and genre.

You can define a general Item interface with those common fields that Book and film will have.

interface Item {
  id: ID!
  name: String! @search
  genre: String! @search
}

type Book implements Item {
  author: String! @search
}

type Film implements Item {
  directed_by: String!
}

Now you can query an Item and request certain fields based on the type of object returned: Book or Film:

queryItem {
  id
  name
  genre
  ... on Book {
    author
  }
  ... on Film {
    directed_by
  }
}

This provides a nice abstraction as it doesn’t matter what type gets returned, as long as we know what interface the type implemented. Interfaces are suitable when dealing with common behaviors among different concrete GraphQL types.

Let’s take a look at another example from GitHub’s GraphQL API where you have the Starrable interface. This interface is implemented by three types:

interface Starrable {
  stargazerCount: Int!
  viewerHasStarred: Boolean!
	...
}

type Gist implements Starrable {...}
type Repository implements Starrable {...}
type Topic implements Starrable {...}

So like the previous example, a query like the following is possible:

query {
  stargazerCount
  viewerHasStarred
  ... on Gist {
    createdAt
    description
  }
  ... on Repository {
    forkCount
    isArchived
  }
  ... on Topic {
    name
  }
}

The types that implement an interface thus can be placed in a query as fragments. Fragments define a set of fields on a type that you can reuse in queries to avoid repetition. In case of interfaces, you use inline fragments to access the underlying type and its fields. Here for example, the ...on Gist fragment is used to access the fields createdAt and description of type Gist.

Now consider the situation when your query could return one of several different objects, without necessarily any overlapping fields among them. That’s where unions come in.

Unions

With unions you can bring completely different types under one union type that could be returned by a field. For example, consider an application where you track your daily tasks. It might use the following example schema:

type StudyItem {
  id: ID!
  subject: String!
  duration: Int!
}

type Meeting {
  id: ID!
  topic: String!
  organizer: User!
  attendents: [User!]!
}

type Shopping {
  id: ID!
  shoppingItems: [ShoppingItems!]!
}

union Task = StudyItem | Meeting | Shopping

type Schedule {
  task: Task
}

Your next task on the schedule could be any one of those three types–either StudyItem, or Meeting or Shopping, and based on what gets returned you’d want the specific details. Declaring Task as a union type, you describe exactly that. Like with interfaces, you can perform queries on the Task union using fragments:

querySchedule {
  ... on StudyItem {
    subject
    duration
  }
  ... on Meeting {
    topic
  }
  ... on Shopping {
    shoppingItems {
      name
      ...
    }
  }
}

The objects under a union type can still have common fields, but unlike interfaces, where you’ve defined what those common fields are, in unions you don’t get to define them. So in queries you have to repeat those common fields inside each fragment. For example StudyItem and Meeting could easily have a duration field , and you’d have to request those fields inside both fragments:

querySchedule {
  ... on StudyItem {
    subject
    duration
  }
  ... on Meeting {
    topic
    duration
  }
  ... on Shopping {
    shoppingItems {
      name
      ...
    }
  }
}

Again you can check out GitHub’s API docs and the union types used, like the Sponsor type. A sponsor could be either an Organization or an individual User. Also notice that these objects have common fields, and implement multiple interfaces; you can have a type that implements an interface under a union type.

In a nutshell:

  • You can use interfaces to bind common traits that are shared by different types; it’s like a contract the terms of which an object must “fulfill” to implement it. This also helps keeping the API clean and performing requests easily.
  • Union types can bring several different objects under one type, regardless of any common set of fields among them. These objects could also implement other interfaces.

Interfaces and Unions with Dgraph

In the latest release, we added support for unions among many other features and fixes. So let’s explore a complete schema deployed by Dgraph’s Slash GraphQL and see how using these two abstract types can help shape up data. In the meantime you can check out Dgraph’s docs on interfaces and unions.

The schema

The idea behind the schema was introduced in a previous article. It represents a simple website where users can post their various artistic works like painting, short films and so on. Beyond users, who are the artists, let’s add patrons of those artists, buyers who can purchase items they like, and art galleries who showcase the artworks.

To accommodate this idea, I can use the following schema:

enum Category {
  Painting
  ShortFilm
  Sculpture
}

interface User {
  username: String! @search @id
  name: String!
}

union Role = Artist | Buyer | Patron | ArtGallery

type Art {
  id: ID!
  category: Category @search
  title: String!
  posted_by: Artist!
}

type Artist implements User {
  submissions: [Art] @hasInverse(field: posted_by)
  patrons: [Patron] @hasInverse(field: patronOf)
}

type Buyer implements User { 
  interests: [Category!]!
  bought: Int!
}

type Patron implements User {
  interests: [Category!]!
  patronOf: [Artist] @hasInverse(field: patrons)
}

type ArtGallery implements User {
  country: String!
}

type Member {
  role: Role
}

The Member type represents registered members of the website; these members can have one of several roles which I’ve brought under the union type Role–they are Artist, Buyer, Patron and ArtGallery. Each of these implements the interface User. I’ve also placed the category of an artwork under an enumeration type Category.

Deploying a GraphQL backend with Slash GraphQL

Slash GraphQL is Dgraph’s managed backend service: production-ready, fully-functional and a painless way to get started with GraphQL application development where you can easily focus on learning and growing as a developer. From just a schema and nothing else, you have a GraphQL API ready to accept requests. So why not have a hands-on experience with what we’ve learned so far?

You can skip this section if you’re already signed up for Slash GraphQL. Otherwise, head over here and start for free.

After signing up, you’ll see an empty dashboard:

Slash GraphQL dashboard after signing up

Slash GraphQL comes with a 7-day free trial. So play and hack around and experiment with ideas. You can start taking the “Interactive Tutorial” and get a guided tour of how to get around with Slash GraphQL. Then if you feel like it, you can upgrade by clicking on the “Upgrade” button in the lower left of the sidebar.

Click on “Launch a backend” for now.

Launch a new Slash GraphQL backend

Fill in the name and hit “Launch”; your backend will be ready in no time.

After deployment, your dashboard will show you some information, including the URL of your server. Note this down—it’s where all your requests would go.

Slash GraphQL dashboard after deployment

Now click on “Schema” on the left sidebar. In the new window paste in the schema and hit “Deploy”.

Slash GraphQL paste schema

And that’s all there’s to it. You have a fully-managed backend serving a GraphQL API. Let’s add some data and dispatch queries to see the new learnings so far in action.

Adding data

Dgraph auto-generates everything else from just your schema–resolvers, mutation and query types etc. I’ll use the addMember mutation to add some Artist, Patron and Buyer type members.

mutation AddMember($memberDetails: [AddMemberInput!]!) {
  addMember(input: $memberDetails) {
    member {
      role {
        __typename
      }
    }
  }
}

With $memberDetails variable holding the Artist type inputs:

{
  "memberDetails": [
    {
      "role": {
        "artistRef": {
          "username": "thomas",
          "name": "Rob Thomas",
          "submissions": [
            {
              "category": "Painting",
              "title": "The Void",
              "posted_by": {
                "username": "thomas"
              }
            }
          ]
        }
      }
    },
    {
      "role": {
        "artistRef": {
          "username": "phill",
          "name": "Phill Collins",
          "submissions": [
            {
              "category": "Painting",
              "title": "The Gazelle",
              "posted_by": {
                "username": "phill"
              }
            },
            {
              "category": "Sculpture",
              "title": "Meditation",
              "posted_by": {
                "username": "phill"
              }
            }
          ]
        }
      }
    },
    {
      "role": {
        "artistRef": {
          "username": "norman",
          "name": "Norman Bates",
          "submissions": [
            {
              "category": "ShortFilm",
              "title": "Songbird",
              "posted_by": {
                "username": "norman"
              }
            },
            {
              "category": "Painting",
              "title": "Sunset Blues",
              "posted_by": {
                "username": "norman"
              }
            }
          ]
        }
      }
    }
  ]
}

Three artists are added with this mutation, so the response should reflect that with the appropriate __typename:

{
  "data": {
    "addMember": {
      "member": [
        {
          "role": {
            "__typename": "Artist"
          }
        },
        {
          "role": {
            "__typename": "Artist"
          }
        },
        {
          "role": {
            "__typename": "Artist"
          }
        }
      ]
    }
  }

In the same way, let’s add a couple of Patron, Buyer and ArtGallery types. The mutation query will be the same, but the input types will change in the JSON variable according to the fields that are required for those objects.

  • For patrons
{
  "memberDetails": [
    {
      "role": {
        "patronRef": {
          "username": "katherine",
          "name": "Katherine Roberts",
          "interests": ["Sculpture"],
          "patronOf": [
            {
              "username": "phill"
            }
          ]
        }
      }
    },
    {
      "role": {
        "patronRef": {
          "username": "tim",
          "name": "Tim Miller",
          "interests": ["Painting", "ShortFilm"],
          "patronOf": [
            {
              "username": "norman"
            },
            {
              "username": "phill"
            },
            {
              "username": "thomas"
            }
          ]
        }
      }
    }
  ]
}
  • For buyers
{
  "memberDetails": [
    {
      "role": {
        "buyerRef": {
          "username": "kirk",
          "name": "Kirk Douglas",
          "interests": ["Sculpture"],
          "bought": 2
        }
      }
    },
    {
      "role": {
        "buyerRef": {
          "username": "rubens",
          "name": "Paul Rubens",
          "interests": ["Painting", "Sculpture"],
          "bought": 4
        }
      }
    }
  ]
}
  • For art gallery types
{
  "memberDetails": [
    {
      "role": {
        "artGalleryRef": {
          "username": "vsg",
          "name": "Vanilla Sky Gallery",
          "country": "Belgium"
        }
      }
    },
    {
      "role": {
        "artGalleryRef": {
          "username": "sideshowsgallery",
          "name": "Sideshows Gallery",
          "country": "England"
        }
      }
    }
  ]
}

Making queries

With some data in the server, I can now dispatch queries. I’ll do a queryMember query. Since the role could be one of several types as declared with the union type Role, I’ll need to use fragments to get appropriate fields based on what type gets returned.

query {
  queryMember {
    role {
      ... on Artist {
        username
        submissions {
          category
          title
        }
      }
      ... on Patron {
        username
        interests
        patronOf {
          username
          submissions {
            title
          }
        }
      }
      ... on Buyer {
        username
        bought
        interests
      }
      ... on ArtGallery {
        name
        country
      }
    }
  }
}

Response:

{
  "data": {
    "queryMember": [
      {
        "role": {
          "username": "norman",
          "submissions": [
            {
              "category": "ShortFilm",
              "title": "Songbird"
            },
            {
              "category": "Painting",
              "title": "Sunset Blues"
            }
          ]
        }
      },
      {
        "role": {
          "username": "thomas",
          "submissions": [
            {
              "category": "Painting",
              "title": "The Void"
            }
          ]
        }
      },
      {
        "role": {
          "username": "phill",
          "submissions": [
            {
              "category": "Painting",
              "title": "The Gazelle"
            },
            {
              "category": "Sculpture",
              "title": "Meditation"
            }
          ]
        }
      },
      {
        "role": {
          "username": "katherine",
          "interests": [
            "Sculpture"
          ],
          "patronOf": [
            {
              "username": "phill",
              "submissions": [
                {
                  "title": "The Gazelle"
                },
                {
                  "title": "Meditation"
                }
              ]
            }
          ]
        }
      },
      {
        "role": {
          "username": "tim",
          "interests": [
            "Painting",
            "ShortFilm"
          ],
          "patronOf": [
            {
              "username": "thomas",
              "submissions": [
                {
                  "title": "The Void"
                }
              ]
            },
            {
              "username": "phill",
              "submissions": [
                {
                  "title": "The Gazelle"
                },
                {
                  "title": "Meditation"
                }
              ]
            },
            {
              "username": "norman",
              "submissions": [
                {
                  "title": "Songbird"
                },
                {
                  "title": "Sunset Blues"
                }
              ]
            }
          ]
        }
      },
      {
        "role": {
          "username": "kirk",
          "bought": 2,
          "interests": [
            "Sculpture"
          ]
        }
      },
      {
        "role": {
          "username": "rubens",
          "bought": 4,
          "interests": [
            "Painting",
            "Sculpture"
          ]
        }
      },
      {
        "role": {
          "name": "Vanilla Sky Gallery",
          "country": "Belgium"
        }
      },
      {
        "role": {
          "name": "Sideshows Gallery",
          "country": "England"
        }
      }
    ]
  }

As you can see, the response stays meaningful to the queried parameters; union types have made it possible to prepare a query that can handle different outcomes and extract proper data, as GraphQL is meant to do.

Summary

Interfaces and unions are part of GraphQL’s simple yet powerful type system that helps developers expose their APIs more effectively. It’s intuitive that they could be combined to create more complex types and represent such behaviors. Now that you know how to use them, you must be having all sorts of crazy ideas! With Slash GraphQL at your disposal, you can start prototyping right away. So sign up if you haven’t done so and take it for a spin.

Resources

Below are some resources you might find helpful: