4. Sharing types and fields: interfaces
20m

Improving the Attraction interface

Earlier, we detailed one of the biggest problems our team is facing: both the Location and Activity types implement an interface called Attraction, but each type has to define the same terrain field separately. This is because in Federation 1, types shared between subgraphs needed to be absolutely identical. This includes interfaces and enums. In this module, we'll see the new flexibility Federation 2 allows us to apply to our app's Attraction interface.

Interface variations

One of the variations Federation 2 allows in a type that is shared between subgraphs is differences in field nullability. This means that a field that is non-nullable in one subgraph's implementation can be nullable in another.

Let's see this in action by adding a new field to the Attraction interface in both the locations and activities subgraphs: minimumAge. This new field will return an Int, and will be required for activities, but optional for locations.

interface Attraction {
id: ID!
"The name of the attraction"
name: String!
"A short description about the attraction"
description: String!
"The attraction's main photo as a URL"
photo: String!
"The minimum age in years for participation on this attraction"
minimumAge: Int! # Optional for locations!
}

In subgraph-activities/activities.graphql:

Adding a new interface field

subgraph-activities/activities.graphql
interface Attraction {
id: ID!
"The name of the attraction"
name: String!
"A short description about the attraction"
description: String!
"The attraction's main photo as a URL"
photo: String!
"The minimum age in years for participation on this attraction"
minimumAge: Int!
}
type Activity implements Attraction @key(fields: "id") {
id: ID!
"The name of the attraction"
name: String!
"A short description about the attraction"
description: String!
"The attraction's main photo as a URL"
photo: String!
"The minimum age in years for participation on this attraction"
minimumAge: Int!
"The activity's terrain"
terrain: ActivityTerrain
"The activity's location"
location: Location
}

In subgraph-locations/locations.graphql:

Adding a new interface field

subgraph-locations/locations.graphql
interface Attraction {
id: ID!
"The name of the attraction"
name: String!
"A short description about the attraction"
description: String!
"The attraction's main photo as a URL"
photo: String!
"The minimum age in years for participation on this attraction"
minimumAge: Int
}
type Location implements Attraction @key(fields: "id") {
id: ID!
"The name of the location"
name: String!
"A short description about the location"
description: String!
"The location's main photo as a URL"
photo: String!
"The minimum age in years for participation on this attraction"
minimumAge: Int
"The location's terrain"
terrain: LocationTerrain
}

With the new field added to the Attraction interface in both of our subgraphs, let's publish our changes to the Apollo schema registry.

Publishing subgraph updates

Now we can return to Explorer and run a query to see the minimumAge return for queried Location and Activity types. (If you see red squiggly lines under the minimumAge field, try refreshing the browser! The new supergraph schema might still be composing.)

query GetMinimumAge {
locations {
id
name
minimumAge
}
activities {
id
name
minimumAge
}
}

We can see in our returned data that the minimumAge value for some locations is actually null! Because the locations subgraph schema defined the Location.minimumAge field as nullable, this is allowed.

Next, we'll verify that our activities subgraph enforces the non-nullability of this field. We'll do this by attempting to return null for a particular activity. Let's make a manual modification to one of our activity objects, and try to query it again.

Task!

Because we've already published our changes to Studio, our gateway server should have retrieved the new supergraph schema by now. Let's try out that query from earlier again.

query GetMinimumAge {
locations {
id
name
minimumAge
}
activities {
id
name
minimumAge
}
}

Uh oh, it doesn't work! While it's permitted for us to receive null as the value for minimumAge on a location, this is a required field for the same query for an activity! The activity object we modified now attempts to return null for the minimumAge, and as we can see, this breaks the rules of our schema.

Let's be sure to revert the change we made to the first activity in the activities_data.json file.

Task!

Omitting fields in shared types

There is one more place we can improve our implementation of the Attraction interface. Let's take a look at where we have this type defined in subgraph-reviews/reviews.graphql.

In this file, we define the Attraction interface to include all of the same fields that appear in both activities and locations schema files. This is because in Federation 1, shared types such as interfaces needed to be identical across subgraphs.

subgraph-reviews/reviews.graphql
interface Attraction {
id: ID!
"The name of the attraction"
name: String!
"A short description about the attraction"
description: String!
"The attraction's main photo as a URL"
photo: String!
}

Federation 2 gives us more flexibility by allowing us to add or omit fields between subgraphs. When a shared type includes fields in one subgraph that are omitted in another, we need to ensure that those fields are always resolvable wherever they might be queried.

Right now, we're implementing the Attraction interface in two types defined in the reviews.graphql file: Activity and Location.

subgraph-reviews/reviews.graphql
type Location implements Attraction @key(fields: "id") {
id: ID!
"The name of the attraction"
name: String! @external
"A short description about the attraction"
description: String! @external
"The attraction's main photo as a URL"
photo: String! @external
"The calculated overall rating based on all reviews"
overallRating: Float
"All submitted reviews for this attraction"
reviews: [Review]!
}
type Activity implements Attraction @key(fields: "id") {
id: ID!
"The name of the attraction"
name: String! @external
"A short description about the attraction"
description: String! @external
"The attraction's main photo as a URL"
photo: String! @external
"The calculated overall rating based on all reviews"
overallRating: Float
"All submitted reviews for this attraction"
reviews: [Review]!
}

Notice that the @external directive is attached to three of the fields on the types implementing this interface: name, description, and photo. This is because the reviews subgraph is not responsible at all for resolving these fields; the locations and activities subgraphs are! We had included name, description and photo in our Attraction definition because of the Federation 1 requirement for shared types to be implemented identically across subgraphs.

We can still rely on the activities and locations subgraphs to resolve these fields for each corresponding type, but with Federation 2 we can cut down on the amount of syntax we're including in reviews.graphql. Let's start by removing these fields from the Attraction interface defined in this file.

Task!

subgraph-reviews/reviews.graphql
interface Attraction {
id: ID!
}

Next, we'll update our implementing types, Activity and Location, to no longer include these fields.

Task!

subgraph-reviews/reviews.graphql
type Location implements Attraction @key(fields: "id") {
id: ID!
"The calculated overall rating based on all reviews"
overallRating: Float
"All submitted reviews for this attraction"
reviews: [Review]!
}
type Activity implements Attraction @key(fields: "id") {
id: ID!
"The calculated overall rating based on all reviews"
overallRating: Float
"All submitted reviews for this attraction"
reviews: [Review]!
}

Our implementing types are much smaller now! But with just one field, id, the Attraction interface isn't too useful anymore.

subgraph-reviews/reviews.graphql
interface Attraction {
id: ID!
}

Luckily, there's one more bit of logic we can relocate. Notice that both the Activity and Location types implement the same reviews-specific fields: overallRating and reviews. Let's copy these fields into our Attraction interface so there's a clear boundary around the logic our implementing types share. (We'll also keep these fields defined for both Activity and Location!)

Task!

subgraph-reviews/reviews.graphql
type Location implements Attraction @key(fields: "id") {
id: ID!
"The calculated overall rating based on all reviews"
overallRating: Float
"All submitted reviews for this attraction"
reviews: [Review]!
}
type Activity implements Attraction @key(fields: "id") {
id: ID!
"The calculated overall rating based on all reviews"
overallRating: Float
"All submitted reviews for this attraction"
reviews: [Review]!
}
interface Attraction {
id: ID!
"The calculated overall rating based on all reviews"
overallRating: Float
"All submitted reviews for this attraction"
reviews: [Review]!
}

Great! We've cut out a bunch of code from our reviews.graphql file. Everything we've left behind is specific to our reviews subgraph, which helps us keep our code modularized.

We've made changes to the reviews subgraph, so let's publish these updates.

Task!

In Explorer, we'll try out a query for latestReviews to make sure our reviews data is still populating.

In this query, we'll use fragments to select specific fields from each one of our implementing types, Activity or Location. If the latest review returned is for an Activity, we'll return its name. For a Location, we'll return its photo URL.

query GetLatestReviews {
latestReviews {
attraction {
... on Activity {
name
}
... on Location {
photo
}
}
}
}

Run the query to see that our interface is working as expected.

Excellent work! 🎉

We've seen how we can include small variations in the definition of an interface between two subgraphs, but that hasn't yet taken care of our team's problem with duplication of logic. We're still defining two different enums - LocationTerrain and ActivityTerrain - and as a result, the terrain field has to be defined on both of our Location and Activity types. In the next lesson, we'll see how Federation 2 helps us overcome this hurdle.

Key takeaways

  • Federation 2 allows shared types to include fields that differ in nullability.
  • Shared types can also include fields in one subgraph that are omitted in another as long as the fields are resolvable wherever they can be queried.
Previous
Next