- Interface
- Inline fragments
- Meta Fields
- Union
- Exercise
- Learning resources
If you are here and followed the previous lessons you pretty much covered the basics of GraphQL, most small projects won't really need what's next, but they certainly will as soon as you have to scale.
Underestimating things is dangerous, especially when it comes to software engineering, and particularly when the same term is used across technologies and you're tempted to assume they work just like the other one works. Interfaces is one of these cases.
Let's start with the spec and gradually break it down and understand how the "Interface" has similarities and differences from e.g. OOP Interface implementation in Java or other language.
GraphQL interfaces represent a list of named fields and their arguments. GraphQL objects can then implement these interfaces which requires that the object type will define all fields defined by those interfaces.
Fields on a GraphQL interface have the same rules as fields on a GraphQL object; their type can be Scalar, Object, Enum, Interface, or Union, or any wrapping type whose base type is one of those five.
Interfaces are an abstract type where there are common fields declared. Any type that implements an interface must define all the fields with names and types exactly matching.
So far is pretty much the same concept you'll see in OOP:
you'll might be tempted to replace the
Type
word withClass
for a mental map but I'd discourage that, it might mislead you
- A common shape abstracted in an
Interface
without the concrete implementation - Many different
Types
, having a shape intersection (common fields) and individual specific characteristics (distinctive fields) which can be identified as individuals AND as part of a wider group, canimplement
the commonInterface
(this is a design decision, having common fields doesn't mean they're necessarily part of a group and sharing and interface) - The
Interface
cannot be used directly but only throughTypes
implementing it - The
Types
must implement all fields defined in theInterface
In a concrete example we'll start seeing how GraphQL Interfaces are similar to OOP and how they're not.
We started with the following example:
input InputCharacter {
homeland: String
kind: Kind
skill: String
}
input InputCharacterUpdateHomelandByKind {
homeland: String!
kind: Kind!
}
enum Kind {
HOBBIT
ELVEN
HALF_ELVEN
ISTARI
}
type Character {
id: ID
name: String
surname: String
age: Int
homeland: String
kind: Kind
friends (input: InputCharacter): [Character!]
progenitor (input: InputCharacter): [Character!]
skill: String
}
type Query {
characters (input: InputCharacter): [Character]
}
But what if later on, we have the need to distinguish the characters? Can we do that in a backwards compatible way? (remember, a "breaking change" is like Chucky, Freddy Krueger, CandyMan and COVID-19 all knocking at your door whilst the Bogyman is coming down the chimney in a Santa's suit ... you ain't do it).
So?
We change the Type
Character
into an Interface
and create the new types implementing it.
interface Character {
id: ID
name: String
surname: String
age: Int
homeland: String
kind: Kind
friends (input: InputCharacter): [Character!]
progenitor (input: InputCharacter): [Character!]
skill: String
}
type Hobbit implements Character {
id: ID
name: String
surname: String
age: Int
homeland: String
kind: Kind
friends (input: InputCharacter): [Character!]
progenitor (input: InputCharacter): [Character!]
skill: String
## Specific Hobbit props
mathoms: [String!]
}
type Elvish implements Character {
... ## for brevity imagine all the Character fields here
## Specific Elvish props
ageLess: Boolean
}
type Istari implements Character {
... ## for brevity imagine all the Character fields here
## Specific Istari props
maiarName: String!
}
type Query {
characters (input: InputCharacter): [Character]
}
At this point you might start making questions like:
- Should I have to repeat all common fields?
- Yes, (◔_◔) so annoying, I know, but remember, you're implementing an interface, you'll see in the next 2 questions how that's relevant.
- I'm still seeing
Character
directly referenced asOutput type
on thecharacters
query operation. Is that correct?- Yup, that's great though! whoever is using the API won't have a breaking change, it'll be transparent!
- Oh, so where the heck the "cannot be directly used" thing fits here?
- (⌐■_■) Exactly !!!!! remember every field in a type will eventually execute a resolver function either explicit or implicit (default)? There you go, the disambiguation happens at resolver level and there's where you end up not-using the Interface directly.
I know, the documentation is not really enlighten on this topic, furthermore, there's not a definition on how to resolve an abstract type on the spec, concretely because is a concern of the server implementation to deal with that.
On chapter 6.4.3 Value Completion of the June2018 spec we can read:
After resolving the value for a field, it is completed by ensuring it adheres to the expected return type. If the return type is another Object type, then the field execution process continues recursively. ...
- CompleteValue(
fieldType
,fields
,result
,variableValues
) ....
- Let objectType be ResolveAbstractType(
fieldType
,result
)
and
Resolving Abstract Types
When completing a field with an abstract return type, that is an Interface or Union return type, first the abstract type must be resolved to a relevant Object type. This determination is made by the internal system using whatever means appropriate.
Note: A common method of determining the Object type for an objectValue in object‐oriented environments, such as Java or C#, is to use the class name of the objectValue.
- ResolveAbstractType(
abstractType
,objectValue
)
- Return the result of calling the internal method provided by the type system for determining the Object type of abstractType given the value objectValue.
... but that's still delegating the concrete resolution to the server implementation.
We'll see how to do it with Apollo due to it's simplicity, taking in consideration other server implementations might defer but they're very similar.
Originally, before adding the interface, we had this:
const resolvers = {
Kind: {
// our Kind enum values mapping
},
Query: {
characters(obj, args, context, info) {
// our characters operation body
}
},
Character: {
friends(obj, args, context, info) {
// our field resolver body
},
progenitor(obj, args, context, info) {
// our field resolver body
}
},
Mutation: {
/// our mutation operations
}
}
But now we can't use the Character
fields resolvers, it's an Interface
, hence we need first to identify the concrete Type and then let GraphQL know that because it doesn't know it at this point. I know it's not really beautiful (it might feel like going back to the dark ages) but the common way is to rely on what makes each type different from the others based on the shape, it'll vary depending on the server implementation, the language and in the end, the team writing the app.
const resolvers = {
Kind: {
// our Kind enum values mapping
},
Query: {
characters(obj, args, context, info) {
// our characters operation body
}
},
Character: {
__resolveType(obj, context, info, resolveType) {
const {
maiarName,
mathoms,
ageLess
} = obj;
if (mathoms) {
return 'Hobbit';
}
if (ageLess) {
return 'Elvish';
}
if (maiarName) {
return 'Istari';
}
},
},
Hobbit: {
friends, // field resolver
progenitor, // field resolver
},
Elvish: {
friends, // field resolver
progenitor, // field resolver
},
Istari: {
friends, // field resolver
progenitor, // field resolver
},
Mutation: {
/// our mutation operations
}
}
I'm oversimplifying this, but if you see how it's described on Apollo's documentation is pretty much that.
Now in order to make a query operation and get the specific values for each type we have to use the same old props for the common fields and inline fragments for the type specific ones and you may also add a specific meta field __typename
to know what's the type of each record:
query Characters{
characters {
__typename
name
homeland
... on Hobbit {
mathoms
}
... on Elvish {
ageLess
}
... on Istari {
maiarName
}
}
}
This is what you'll get:
{
"data": {
"characters": [
{
"__typename": "Hobbit",
"name": "Frodo",
"homeland": "The Shire",
"mathoms": [
"spoon",
"lamp"
]
},
{
"__typename": "Hobbit",
"name": "Sam",
"homeland": "The Shire",
"mathoms": [
"shovel",
"bucket"
]
},
{
"__typename": "Hobbit",
"name": "Meriadoc",
"homeland": "Buckland",
"mathoms": [
"table",
"chairs"
]
},
{
"__typename": "Hobbit",
"name": "Peregrin",
"homeland": "The Shire",
"mathoms": [
"door"
]
},
{
"__typename": "Elvish",
"name": "Arwen",
"homeland": "Rivendell",
"ageLess": true
},
{
"__typename": "Elvish",
"name": "Elrond",
"homeland": "Rivendell",
"ageLess": true
},
{
"__typename": "Elvish",
"name": "Celebrían",
"homeland": "Valinor",
"ageLess": true
},
{
"__typename": "Hobbit",
"name": "Drogo",
"homeland": "The Shire",
"mathoms": []
},
{
"__typename": "Hobbit",
"name": "Saradoc",
"homeland": "Buckland",
"mathoms": []
},
{
"__typename": "Hobbit",
"name": "Esmeralda",
"homeland": "Buckland",
"mathoms": []
},
{
"__typename": "Istari",
"name": "Gandalf",
"homeland": "",
"maiarName": "Olórin"
},
{
"__typename": "Istari",
"name": "Saruman",
"homeland": "",
"maiarName": "Curumo"
},
{
"__typename": "Istari",
"name": "Radagast",
"homeland": "",
"maiarName": "Aiwendil"
},
{
"__typename": "Elvish",
"name": "Galadriel",
"homeland": "Lothlorien",
"ageLess": true
}
]
}
}
Alternatively you can query which types are implementing a certain Interface using Schema Introspection:
query UnionInterfaceTypes {
__type(name: "Character") {
possibleTypes {
name
kind
}
}
}
{
"data": {
"__type": {
"possibleTypes": [
{
"name": "Hobbit",
"kind": "OBJECT"
},
{
"name": "Elvish",
"kind": "OBJECT"
},
{
"name": "Istari",
"kind": "OBJECT"
}
]
}
}
}
So far so good? Let's make it a little harder.
Ready?
A type can implement multiple interfaces
You may say "yeah... (◔_◔) whatever, piece of cake".
Not so fast!
## we add a new interface
interface MagicalCreature {
magicPowers: [String!]
}
### and update 2 types to implement it
type Elvish implements Character & MagicalCreature{
... ## all previous fields remain unchanged
## the new field
magicPowers: [String!]
}
type Istari implements Character & MagicalCreature{
... ## all previous fields remain unchanged
## the new field
magicPowers: [String!]
}
type Query {
... ## all previous query operations
magical: [MagicalCreature]
}
Now the resolvers
const resolvers = {
Kind: {
// our Kind enum values mapping
},
Query: {
characters(obj, args, context, info) {
// our operation body
},
magical(obj, args, context, info) {
// our operation body
}
},
Character: {
__resolveType({
maiarName,
mathoms,
ageLess
}, context, info, returnType) {
if (mathoms) {
return 'Hobbit';
}
if (ageLess) {
return 'Elvish';
}
if (maiarName) {
return 'Istari';
}
},
},
MagicalCreature: {
__resolveType({
maiarName,
ageLess
}, context, info, returnType) {
if (ageLess) {
return 'Elvish';
}
if (maiarName) {
return 'Istari';
}
},
},
Hobbit: {
friends, // field resolver
progenitor, // field resolver
},
Elvish: {
friends, // field resolver
progenitor, // field resolver
},
Istari: {
friends, // field resolver
progenitor, // field resolver
},
Mutation: {
/// our mutation operations
}
}
As you can see, we start having a LOT of repeated code, and because of the __resolveType
implementation forcing you to return a string with the name of a type, you might end having to deal with the return order (probably due to a "not very refined" design on your type definition). A lot of attention and planning is required to avoid derailing here, and trust me, it can happen really fast.
Unions are an abstract type where no common fields are declared. The possible types of a union are explicitly listed out in possibleTypes. Types can be made parts of unions without modification of that type.
Source: GraphQL spec - June 2018 - Union
GraphQL Unions represent an object that could be one of a list of GraphQL Object types, but provides for no guaranteed fields between those types. They also differ from interfaces in that Object types declare what interfaces they implement, but are not aware of what unions contain them.
With interfaces and objects, only those fields defined on the type can be queried directly; to query other fields on an interface, typed fragments must be used. This is the same as for unions, but unions do not define any fields, so no fields may be queried on this type without the use of type refining fragments or inline fragments.
- Abstract types
- Represent a list of arbitrary Object types
- No common fields are declared
- Provides for no guaranteed fields between Object Types listed in a Union type
- Object types not aware of unions containing them.
- Being an abstract type implies disambiguation happening at runtime on your resolvers (depending on the technology but usually achieved as Interfaces do, e.g.
__resolveType(...)
).
union MyUnion = ObjectA | ObjectB | ObjectC
Also Union members may be defined with an optional leading |
character to aid formatting when representing a longer list of possible types:
union MyUnion =
| ObjectA
| ObjectB
| ObjectC
| ObjectD
| ObjectE
What's the point of having a type for potentially unrelated Objects?
Let's say, in our LOTR example, you want to create a global search query operation for your blog.
You'll have:
- Objects sharing a common interface like
Character
- Objects with shared fields (e.g.
name
) but not sharing a logical hierarchy likeSword
,Realm
,City
,Author
,Reviewer
,User
- Objects with completely different shapes like
Review
,Comment
Doing something like this is completely valid!
union GlobalSearchResult =
| Character
| Sword
| Realm
| City
| Author
| Reviewer
| User
| Review
| Comment
type Query {
globalSearch: [GlobalSearchResult]
}
and
const resolvers = {
GlobalSearchResult: {
__resolveType(obj, context, info, returnType){
if (obj.bladeLength){
return 'Sword';
}
/// and so on
},
},
Query: {
globalSearch: () => { ... }
},
};
Can I combine a Union and an Interface to guarantee an intersection?
Technically yes.
interface Searchable {
name: String
}
union GlobalSearchResult =
| Character ## implements Searchable
| Sword ## idem
| Realm ## idem
| City ## idem
| Author ## idem
| Reviewer ## idem
| User ## idem
| Review ## OOPS, what here?
| Comment ## OOPS, what here?
type Query {
globalSearch: [GlobalSearchResult]
}
You stated every Searchable
Object should implement the name
property, ... that's fine, it's declarative, but what's the point of having a Union here? You could make it completely restrictive and get rid of the union entirely ... but ... you'll miss the Review
and Comment
because it doesn't make any sense to have a name
property there at all.
...
For a given datasource (abstracted as json here) containing n
rows of skills
and n
rows of persons
we provided a sample implementation of a GraphQL server for each technology containing:
- a server app
- a schema
- a resolver map
- an entity model
- a db abstraction
The code contains the solution for previous exercises so you can have a starting point example.
This is a long one, keep it simple and put all your attention on the GraphQL aspects. Unless you're blocked, write down your questions so you can have the opportunity to discuss them later.
- Add a
Grade
enum with some values of your choice (e.g.TRAINEE
,JUNIOR
,SENIOR
) - Add a
Role
enum with some values of your choice (e.g.DEVELOPER
,SDET
,TEAM_LEAD
) - Change
Person
type to anInterface
- Add an
Employee
Interface
with a non-nullableemployeeId
property of typeID
- Add a
Contact
type implementingPerson
interface with no extra properties - Add a
Candidate
type implementingPerson
interface with 2 extra properties- non-nullable
targetRole
of typeRole
- non-nullable
targetGrade
of typeGrade
- non-nullable
- Add an
Engineer
type implementingPerson
andEmployee
interfaces with 2 extra properties- non-nullable
role
of typeRole
- non-nullable
grade
of typeGrade
- non-nullable
- Add a
GlobalSearch
union listingSkill
,Contact
,Candidate
andEngineer
object types - Add an
InputCandidateCreate
input with the same propertiesInputPersonCreate
input has with some changes:surname
,email
,age
should be non-nullable- add non-nullable
targetRole
of typeRole
- add non-nullable
targetGrade
of typeGrade
- Add an
InputEngineerCreate
input with the same propertiesInputPersonCreate
input has with some changes:surname
,email
,age
should be non-nullable- add non-nullable
role
of typeRole
- add non-nullable
grade
of typeGrade
- Add an
InputGlobalSearch
input with a non-nullablename
property of typeString
- Update the
InputPerson
input with the following properties:- nullable
targetRole
of typeRole
- nullable
targetGrade
of typeGrade
- nullable
role
of typeRole
- nullable
grade
of typeGrade
- nullable
- Add a
search
query operation with aninput
argument of typeInputGlobalSearch
which returns a nullable list ofGlobalSearch
union types - Add a
createCandidate
mutation operation with aninput
argument of typeInputCandidateCreate
which returns a non-nullableCandidate
object types - Add a
createEngineer
mutation operation with aninput
argument of typeInputEngineerCreate
which returns a non-nullableEngineer
object types
- Add a
createCandidate
mutation resolver (use thecreatePerson
resolver as a guide) - Add a
createEngineer
mutation resolver (use thecreatePerson
resolver as a guide) - Add a
GlobalSearch
Root Operation definition with a type resolver to return the right Object type listed in the union type. This will change a lot depending on the language and the server implementation, e.g. with Apollo Server it's the__resolverType
resolver. - Add
Person
andEmployee
Root Operation definitions with a type resolver to return the right Object type for the Object types implementing the interfaces. This will change a lot depending on the language and the server implementation, e.g. with Apollo Server it's the__resolverType
resolver. - Add
Contact
,Candidate
, andEngineer
Root Operation definitions with the required field resolvers (hint: they were initially on thePerson
Root Operation before changing it to an interface). - Add a
search
query operation which returns the matching results on the persons and skills records (take a look at the Person and Skill models, they should provide a functionality to perform the concrete search)
- All previous operations (from previous days) MUST still work
- All queries related to the
Person
interface can now support inline fragments to request fields for specific types, try some options and see what happens (e.g. on theglobalSearch
operation). - Don't bother about validations except for those provided by GraphQL and only GraphQL.
## Part 1
mutation createCandidate(
$name: String!
$surname: String!
$email: String!
$age: Int!
$eyeColor: EyeColor
$friends: [ID!]
$skills: [ID!]
$favSkill: ID
$targetRole: Role!
$targetGrade: Grade!
) {
createCandidate(
input: {
name: $name
surname: $surname
email: $email
age: $age
eyeColor: $eyeColor
friends: $friends
skills: $skills
favSkill: $favSkill
targetRole: $targetRole
targetGrade: $targetGrade
}
) {
id
fullName
email
age
eyeColor
targetRole
targetGrade
}
}
## Part 2
mutation createEngineer(
$name: String!
$surname: String!
$email: String!
$age: Int!
$eyeColor: EyeColor
$friends: [ID!]
$skills: [ID!]
$favSkill: ID
$role: Role!
$grade: Grade!
) {
createEngineer(
input: {
name: $name
surname: $surname
email: $email
age: $age
eyeColor: $eyeColor
friends: $friends
skills: $skills
favSkill: $favSkill
role: $role
grade: $grade
}
) {
id
employeeId
fullName
email
age
eyeColor
role
grade
}
}
## Part 3
query globalSearch {
search (input: {name: "a"}){
__typename
... on Person {
name
}
... on Skill {
name
}
}
}
Select the exercise on your preferred technology:
- GraphQL Spec (June 2018)
- GraphQL Org
- Other articles