More Sophisticated Schemas with Advanced GraphQL Features

Written By
David Choi

GraphQL has become a widely adopted standard over the past few years. However, not everyone is taking advantage of the full capabilities of the standard. In this article, I’ll demonstrate methods for creating more expressive schemas that better fit your data needs. 

 

Let’s start by defining an example scenario. We’re building a forum application, which will have several types of users:

  • Standard users that post and read content.  
  • Moderators that oversee forum postings and police activity.  
  • Administrators that manage the operations of the forum. For example, dealing with technical issues, assigning moderators, and more.

 

What might a schema for such a site look like? Let’s start off with a barebones schema. This example will be using a Node Express and GraphQL project. I’ll assume you already know how to do that, but I will include the entire project code at the bottom of the article later. Create a typeDefs.ts file like this. Notice that we’re using TypeScript, since the type annotations help make the code more clear. 

 


import { gql } from "apollo-server-express"; 
 
const typeDefs = gql` 
  type StandardUser { 
    id: ID! 
    userName: String! 
    stars: Int! 
  } 
 
  type Moderator { 
    id: ID! 
    userName: String! 
    stars: Int! 
    categories: [Category!] 
  } 
 
  type Administrator { 
    id: ID! 
    userName: String! 
  } 
 
  type Category { 
    id: ID! 
    name: String! 
  } 
`; 
 
export default typeDefs; 

You’ll notice that we have the three user types, and an additional type called Category, which represents the group that our posts will go into. However it’s clear there is repetition across our fields (e.g. id, username). It’s in fact possible to pull out a “base type” from each of the three user related types. Let’s attempt to do this by updating this schema using Interfaces.  

 

Interfaces 

 

An interface is just a skeleton of sorts. It’s a contract that declares rules, in this case specific fields, however it has no implementation and therefore cannot return data. Only types can. However since it does provide structure, we are using Interfaces to define base fields and then have our types implement those Interfaces. For example, if we update our typeDefs with Interfaces it could look like this. 


const typeDefs = gql` 
  interface Identifiable { 
    id: ID! 
  } 
 
  interface User implements Identifiable { 
    id: ID! 
    userName: String! 
  } 
 
  interface Rating { 
    stars: Int! 
  } 
 
  type StandardUser implements Identifiable & User & Rating { 
    id: ID! 
    userName: String! 
    stars: Int! 
  } 
 
  type Moderator implements Identifiable & User & Rating { 
    id: ID! 
    userName: String! 
    stars: Int! 
    categories: [Category!] 
  } 
 
  type Administrator implements Identifiable & User { 
    id: ID! 
    userName: String! 
  } 
 
  type Category implements Identifiable { 
    id: ID! 
    name: String! 
  } 
`; 

If we start at the top, you’ll see our first interface is called Identifiable. Since all of the entities in our data model have a unique ID, this interface will be the base for all of our entities. Next, we see that the User interface implements the Identifiable interface. Notice how User has both the id field and its own field called userName. When a type or interface implements an interface, they must implement all of its fields.  

 

Next up we have another interface called Rating. Since two of our types StandardUser and Moderator use the stars field, but Administrator does not, we have this interface so that only the types that use the stars field can implement it – otherwise, the User models can skip it. 

 

Now, we finally have our actual types StandardUser, Moderator, and Administrator, where each one implements only the interfaces it needs by using an & symbol to denote more than one interface being implemented. Then lastly the Category type. 

 

So, as you can see this set of types and hierarchies is quite reminiscent of most modern programming languages, for example TypeScript.

 

We now have a reasonable schema model. Let’s try querying it. Update the typeDefs variable by adding this Query. 


type Query { 
    getAllUsers: [User!] 
} 

 

As you can see, we’ve added a function that retrieves all of our site’s users, but returns them as an array of Users. Let’s now write our resolver and see if this will work. First create your resolver file like the following:


import { IResolvers } from "apollo-server-express"; 
import { getAllUsers, User } from "../dataService"; 
import { GqlContext } from "./GqlContext"; 
 
const resolvers: IResolvers = { 
  Query: { 
    getAllUsers: async ( 
      parent, 
      args: null, 
      ctx: GqlContext, 
      info: any 
	): Promise> => { 
      return getAllUsers(); 
	}, 
  }, 
}; 
 
export default resolvers; 

Our resolver’s only action is to call our getAllUsers function in order to retrieve the actual data. You’ll notice that our getAllUsers resolver is returning an array of User Interfaces. This should be a bit suspicious, but for now let’s just see what happens. 

 

Now we must implement the getAllUsers function in code. But to make this article simpler and focus on GraphQL we’ll create a function that returns hard coded values. Create a file called dataService.ts and add this code.

 


import { v4 } from "uuid"; 
 
export interface Identifiable { 
  id: string; 
} 
 
export interface User extends Identifiable { 
  userName: string; 
} 
 
export interface Rating { 
  stars: number; 
} 
 
export class StandardUser implements User { 
  constructor( 
    public id: string, 
    public userName: string, 
    public stars: number 
  ) {} 
} 
 
export class Moderator implements User { 
  constructor( 
    public id: string, 
    public userName: string, 
    public stars: number, 
    public categories: [Category] 
  ) {} 
} 
 
export class Administrator implements User { 
  constructor(public id: string, public userName: string) {} 
} 
 
export class Category implements Identifiable { 
  constructor(public id: string, public name: string) {} 
} 
 
export function getAllUsers() { 
  const users: Array = []; 
  users.push(new StandardUser(v4(), "dave", 4)); 
  users.push(new Moderator(v4(), "ruth", 5, [new Category(v4(), "Cooking")])); 
  users.push(new StandardUser(v4(), "jane", 5)); 
  users.push(new StandardUser(v4(), "tom", 1)); 
  users.push(new Administrator(v4(), "linda")); 
  users.push( 
    new Moderator(v4(), "tom", 2, [new Category(v4(), "Programming")]) 
  ); 
  users.push(new Administrator(v4(), "betty")); 
 
  return users; 
} 

Our dataService contains all of our TypeScript types that mirror our GraphQL schemas, as well as the getAllUsers function that returns a list of objects that inherit from our User Interface. As you can see, getAllUsers all types of Users, including StandardUsers and Administrators, but since they all implement the User interface (i.e. User is the parent type for all of them), the function’s return type is set as an array of User types. Now let’s try querying it!

Querying and Fragments

Open your GraphQL playground by opening your browser to the GraphQL server URL, which would be something like http://localhost:<port number>/graphql. Then add this Query as shown. 


query { 
    getAllUsers { 
  	... on User { 
        id 
        userName 
  	} 
	} 
} 

This is our Query for getAllUsers, but instead of fields we see three periods. What we’re seeing with the “…” is called an inline Fragment. This tells the GraphQL service which type of data we are looking for. In this case we indicate that we want the Interface User and its fields of id and userName. 

 

So let’s try and run this Query, but when we do we get this error shown. 

 

Graphical user interface, text, applicationDescription automatically generated
Figure 1.6 First query error 

 

What does this error mean? If we look back at our definition of GraphQL Interfaces we said they only have declarations, but no implementations. Therefore, we need to replace the interface with an implementation of the interface, the type, that does return data. Let’s see how to do that. 

 

Update the resolvers variable by adding this code. 


User: { 
    __resolveType(obj: any, ctx: GqlContext, info: any) { 
      if (obj.categories) { 
        return "Moderator"; 
  	} else if (obj.stars) { 
        return "StandardUser"; 
  	} 
 
      return "Administrator"; 
	}, 
  }, 

 

By defining the User Interface in our resolvers we are telling GraphQL that when it encounters the User in a Query it should instead return one of the specific types listed. Note, the logic of how to go about determining this is entirely up to you. However an obvious potential way of doing this would be to use member fields unique to each type, as we have done here. 

 

So now when we run the same Query again we get this. 

 

Graphical user interface, text, applicationDescription automatically generated
Figure 1.8 Second query updated attempt 

Clearly this works, but we’re not getting all the related fields and it’s hard to tell which type each returned item is. Let’s update our query a bit to show these details. 

 

Graphical user interface, text, applicationDescription automatically generated
Figure 1.9 Updated Query

  

If we go over this Query, we can see that by using the __typename field we get back the type name of every item in our result set. So that definitely helps clarify things somewhat. However only the StandardUser type is returning actual fields. The other types are indicated, but their type-specific data is blank! It’s clear what we need to do so let’s take a stab at it.

TextDescription automatically generated
Figure 1.10 Updated Query with all types 

  

As you probably guessed, we needed to add our other types and their specific fields. For example, in the case of Moderator we have a unique field called categories, Since the category is its own type as well, we also specified the “name” member from the type.

 

Now we’ve got everything working! Alas, there’s one small issue. Even after implementing such robust types, we’re still repeating the id and userName fields multiple times. In this case, it doesn’t matter much. But for larger data models, it’s quite easy to have dozens of shared fields – imagine specific all those fields repeatedly!

 

We’ve already seen how we can use inline Fragments to indicate the fields we want for a specific type, but we can also use Fragments to help reduce repetition. Let’s update our Query one more time like this. 

 

TextDescription automatically generated
Figure 1.11 Updated Query with Fragment 

 

That’s much better. We were able to remove the redundancy and make our code cleaner. Now let’s make one more change to improve things further.

 

Unions 

 

A Union in GraphQL is a type that could be one of several type definitions. For example, this is what a Union for our schema types could look like. 


union Result = StandardUser | Moderator | Administrator 
 
  type Query { 
    getAllUsers: [Result!] 
  } 

If we now make our getAllUsers Query function return a Result Union instead of the User Interface, it becomes much clearer what specific types we were intending to return. Additionally, in larger schemas, it is possible that we may want to have Unions for different purposes so that one Union may only contain two of several types in our schema while another Union might contain several other types. 

 

Now that we’ve created a Union, we also need to create a resolver for it. For our schema the resolver implementation can actually be the same as the User resolver. It would look like this. 


Result: { 
    __resolveType(obj: any, ctx: GqlContext, info: any) { 
      if (obj.categories) { 
        return "Moderator"; 
  	} else if (obj.stars) { 
        return "StandardUser"; 
  	} 
 
      return "Administrator"; 
	}, 
  }, 

If you re-run the last query as is, it should work the same as before. In the case of our sample app, it happens to be the same implementation. In a more complete and realistic schema, it can and would be different – we’d have many different types, with differences in the way they could be combined together into a Union. 

 

In this tutorial, you were introduced to Interfaces, Fragments, and Unions. Each feature can help improve your schemas to more precisely align to your data needs and be more easily queryable. 

 

Source code can be found at https://github.com/jsoneaday/advanced-gql-features

 

Find more articles on my Blog https://jsoneaday.com/ 


Subscribe for more posts like this one