DEV Community

Eve Porcello
Eve Porcello

Posted on

Enhancing GraphQL Schemas with Interfaces

This article was originally posted on Moon Highway.

At the heart of any GraphQL project is a schema, a document that describes all of the types, queries, mutations, and subscriptions that are available on the API. In addition to these basic types, the GraphQL schema definition language provides us with a way to create an interface.

The concept of an interface is often present in object-oriented languages. They define the properties and methods that an object must implement in order to communicate with other objects. For example, the traffic light doesn't need to know about the inner workings of every vehicle on the road. However, every vehicle on the road does need to stop when the light is red and go when the light is green. We could define an interface for a traffic light that requires every vehicle on the road to implement stop() and go() methods. So long as a car, bus, or pedestrian has methods defined for stop() and go() they will be able to interface with traffic lights.

In GraphQL, an interface serves a similar purpose. Instead of properties and methods, GraphQL interfaces require types to implement specific fields. Let's take a look at what this means by improving an already existing schema by incorporating interfaces.

We already have a schema set up for ski resort employees. We could improve this schema by using interfaces. At this resort, we have a type Employee that has the following fields:

type Employee {
  id: ID!
  firstName: String!
  lastName: String!  
  job: JobType
}

enum JobType {
  LIFTOPERATOR
  SKIPATROL
  INSTRUCTOR
  BARTENDER
}

At first glance, this looks good, but there is room for improvement. A lift operator and a bartender might have a few shared fields, but there are probably unique fields for each of them. Let's take a look at a sample data record for each job. There are role-specific fields that we want to make available in the API:

bartenders.json

[
  {
    "id": "0001",
    "firstName": "Topher",
    "lastName": "Saunders",
    "assignment": "SUMMIT",
    "supervisor": true,
    "shift": 1
  }
]

instructors.json

[
  {
    "id": "0002",
    "firstName": "Matt",
    "lastName": "Christie",
    "level": 3,
    "privateLessons": true
  }
]

liftops.json

[
  {
    "id": "0008",
    "firstName": "Shawni",
    "lastName": "Horizon",
    "yearsExperience": 1
  }
] 

patrol.json

[
  {
    "id": "0007",
    "firstName": "Denise",
    "lastName": "Lankman",
    "certified": true,
    "aviLevel": 3
  }
]

We can enhance our schema by using a GraphQL interface. We'll start by converting the Employee type to be an Employee interface. This will include the base fields that every employee will have:

interface Employee {
  id: ID!
  firstName: String!
  lastName: String!
}

Next, we'll create implementations of this interface for each of the employee types. All of these new types will need to implement the same fields that the interface does (id, firstName, and lastName), and then fields unique to the job role can be added to each type:

type Bartender implements Employee {
  id: ID!
  firstName: String!
  lastName: String!
  assignment: Location!
  supervisor: Boolean!
  shift: Int!
}

type Instructor implements Employee {
  id: ID!
  firstName: String!
  lastName: String!
  level: Int!
  privateLessons: Boolean!
}

type LiftOperator implements Employee {
  id: ID!
  firstName: String!
  lastName: String!
  yearsExperience: Int!
}

type SkiPatrol implements Employee {
  id: ID!
  firstName: String!
  lastName: String!
  certified: Boolean!
  aviLevel: Int!
}

If we want to query a list of everyone who works at Snowtooth Mountain regardless of their job role, we can now define that query using the Employee interface:

type Query {
  allEmployees: [Employee!]!
}

In order to make this work, we need to reflect this change in the resolvers. We'll need to add a __resolveType resolver, that will return the name of the type that is being resolved:

const resolvers = {
  Query: {...},
  Mutation: {...},
  Employee: {
    __resolveType: parent => {
      if (parent.assignment) {
        return "Bartender";
      } else if (parent.yearsExperience) {
        return "LiftOperator";
      } else if (parent.certified) {
        return "SkiPatrol";
      } else {
        return "Instructor";
      }
    }
  }
};

When resolving employees, the __resolveType resolver will look at the parent data and decide what type of Employee is being resolved. In this case, we are checking the parent object to see if it contains fields that relate to each type. For example, if the parent has a field for certified, then they type must be SkiPatrol.

Now we can query a list of all Snowtooth employees regardless of type:

query {
  allEmployees {
    id
    firstName
    lastName
  }
}

The __typename filed can be added to our selection set to see what type of employee we are dealing with:

query {
  allEmployees {
    __typename
    id
    firstName
    lastName
  }
}

And, in the same query we can still ask for specific data about each individual type using inline fragments:

query {
  allEmployees {
    __typename
    id
    firstName
    lastName
    ... on SkiPatrol {
      certified
    }
  }
}

The data returned from this query will have id, firstName, lastName for all employees and certified for just ski patrol employees. The JSON response from the query would look like this:

[
  {
    "__typename": "SkiPatrol",
    "id": "0011",
    "firstName": "Jill",
    "lastName": "Johnson",
    "certified": "true"
  },
    {
    "__typename": "Bartender",
    "id": "0012",
    "firstName": "Rebecca",
    "lastName": "Wilson"
  }
  ...
]

Interfaces also make your API more scalable and maintainable. When a new job role comes along, you can create formalized object types for that role that implements the Employee interface. That will make the new type available on all fields that resolve employees anywhere in your schema.

Another benefit is that you can still use the interface types independently wherever you choose:

type Query {
  allBartenders: [Bartender!]!
  allInstructors: [Instructor!]!
  allLiftOperators: [LiftOperator!]!
  allSkiPatrol: [SkiPatrol!]!
}

Finally, let's try one more query, an introspection query that shows what types implement Employee:

query UnionInterfaceTypes {
  __type(name: "Employee") {
    possibleTypes {
      name
      kind
    }
  }
}

This query returns a list of every type that implements the Employee interface:

{
  "data": {
    "__type": {
      "possibleTypes": [
        {
          "name": "Bartender",
          "kind": "OBJECT"
        },
        {
          "name": "Instructor",
          "kind": "OBJECT"
        },
        {
          "name": "LiftOperator",
          "kind": "OBJECT"
        },
        {
          "name": "SkiPatrol",
          "kind": "OBJECT"
        }
      ]
    }
  }
}

When you're modeling your domain's objects with GraphQL, an interface is a useful structure to understand. Don't hesitate to start using them today.

Top comments (7)

Collapse
 
mikemeerschaert profile image
mikemeerschaert

What if Jill Johnson happens to fill the role of instructor and ski patrol? Can a single record resolve to multiple types so you can get fields of both types on the same object?

So would it be possible to run this query:

query {
allEmployees {
__typename
id
firstName
lastName
... on SkiPatrol {
certified
}
... on Instructor {
privateLessons
}
}
}

and return the result:
[
{
"__typename": ["SkiPatrol","instructor"],
"id": "0011",
"firstName": "Jill",
"lastName": "Johnson",
"certified": "true",
"privateLessons": "true"
},
]

Collapse
 
eveporcello profile image
Eve Porcello

With the schema set up as it is currently, this wouldn't be possible, but we could adjust to allow for a person to hold multiple types of roles. If you wanted a single record to result to multiple types, you'd need to create a union. Then we could create a field for say roles which could return a list of different types for a particular field.

You can learn more about unions and interfaces in the GraphQL docs: graphql.org/learn/schema/

Collapse
 
mikemeerschaert profile image
mikemeerschaert

Thank you for the reply! I was reading over the union documentation and from what I understand you can union multiple types together so different types can be returned on the same field, but I'm not sure that would resolve the issue as we'd need a type with the certified field from SkiPatrol, and the privateLessons field from instructor on the same data item.

so you might have in your schema:

enum Role{
SkiPatrol
Instructor
}

interface Employee {
id: ID!
firstName: String!
lastName: String!
role: [Role!]!
}

type Instructor implements Employee {
id: ID!
firstName: String!
lastName: String!
level: Int!
privateLessons: Boolean!
role: [Role!]!
}

type SkiPatrol implements Employee {
id: ID!
firstName: String!
lastName: String!
certified: Boolean!
aviLevel: Int!
role: [Role!]!
}

type Query {
allEmployees: Instructor | SkiPatrol
}

and you run the same query i mentioned above:

query {
allEmployees {
__typename
id
firstName
lastName
... on SkiPatrol {
certified
}
... on Instructor {
privateLessons
}
}
}

You'd still have the problem where the employee with id 0011 must be either a SkiPatrol (with the certified field) or instructor (with the privateLessons field) but I don't see how you'd be able to return someone with those fields from both types into an object that looks like this:

[
{
"roles": ["SkiPatrol","instructor"],
"id": "0011",
"firstName": "Jill",
"lastName": "Johnson",
"certified": "true", <--I want both of these fields on the same record
"privateLessons": "true" <--I want both of these fields on the same record
},
]

I think a better way to rephrase my question would be: Is there a way to have types that compose multiple other types? Similar to how in object oriented programming you can have a class that inherits multiple other classes to combine all methods and properties from both classes.

Thread Thread
 
jaxolotl profile image
Javier Valderrama (Jax)

This is a really interesting question. I've been looking at the implementation and didn't find a viable solution so far. The problem seems to be related to the way the __resolveType method is implemented. It's designed to force you to return a type name as a String and just one. Clearly that's part of the GraphQL declarative nature

    __resolveType(obj, context, info) {

      const {
        certified,
        privateLessons
      } = obj;

      if (certified) {
        return 'SkiPatrol';
      }

      if (privateLessons) {
        return 'Instructor';
      }

      return 'WateverDefault';
    },

And depending on the order of the early returns you'll end up having the first match.

A possible workaround might be to have multiple interfaces:

interface Employee {
  id: ID!
  firstName: String!
  lastName: String!
  role: [Role!]!
}

interface InstructorInterface {
  level: Int!
  privateLessons: Boolean!
}

interface SkiPatrolInterface {
  certified: Boolean!
  aviLevel: Int!
}

type Instructor implements Employee & InstructorInterface {
  id: ID!
  firstName: String!
  lastName: String!
  role: [Role!]!
  ##
  level: Int!
  privateLessons: Boolean!
}

type SkiPatrol implements Employee & SkiPatrolInterface{
  id: ID!
  firstName: String!
  lastName: String!
  role: [Role!]!
  ##
  certified: Boolean!
  aviLevel: Int!
}

type SkiPatrolAndInstructor implements Employee & SkiPatrolInterface & InstructorInterface{
  id: ID!
  firstName: String!
  lastName: String!
  role: [Role!]!
  ##
  certified: Boolean!
  aviLevel: Int!
  ##
  level: Int!
  privateLessons: Boolean!
}

and resolve it like this

    __resolveType(obj, context, info) {

      const {
        certified,
        privateLessons
      } = obj;

      if (certified && privateLessons) {
        return 'SkiPatrolAndInstructor';
      }

      if (certified) {
        return 'SkiPatrol';
      }

      if (privateLessons) {
        return 'Instructor';
      }

      return 'WateverDefault';
    },

obviously this is just a hint and probably not the best practice for production quality code, but it might help you think something more elegant

Thread Thread
 
mikemeerschaert profile image
mikemeerschaert • Edited

Thanks for the reply! This is actually exactly how I ended up implementing my schema! I have a file called CombinationTypes that defines all the different permutations of combined interfaces. When new combos are needed it's as simple as adding them to the schema since the interfaces already have the resolvers mapped to them. I have a "base" interface (like you have here with employee) that every combination type implements, and then I have a json object that maps possible values of fields on the base interface to all the various combo types. It's not in production yet, but it actually works very well.

Thread Thread
 
jaxolotl profile image
Javier Valderrama (Jax)

Maybe I'm not getting it right, but it sounds to me as something you might achieve in a more GraphQL style by using Federation, Schema Composition and Apollo Gateway.

Here a link to the documentation and 2 excellent videos

apollographql.com/docs/apollo-serv...

youtube.com/watch?v=v_1bn2sHdk4

youtube.com/watch?v=OFT9bSv3aYA

I hope you find this useful.

Collapse
 
eveporcello profile image
Eve Porcello

Hey thanks for reading this and the comment here.

That's a good way to think about it. An interface is like a parent object and then the children types have all of the fields of the parent plus additional fields. I have some slides that show a little more detail about this here.

Hope that helps!