Extending entities
To view this content, get the Pro package! 😃🙏
Or if you’ve already purchased.
Extending entities
If you’re jumping in here,
git checkout federation3_0.1.0(tag federation3_0.1.0, or compare federation3...federation4)
In this section, we’ll build another service—this one for reviews—and we’ll see how to extend entities created by other services. Then, we’ll add the reviews service to the gateway and see how the gateway resolves queries involving both services.
Let’s start with the schema. First, we take the Review type and reviews query from our monolith for our new schema, and then we add a few things:
import { gql } from 'apollo-server'
export default gql`
scalar Date
type Review @key(fields: "id") {
id: ID!
text: String!
stars: Int
author: User!
createdAt: Date!
updatedAt: Date!
}
extend type Query {
reviews: [Review!]!
}
extend type User @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
}
`scalar Date, as we did in theusersservice@keydirective fortype Review, to declare it as a federation entityextend type User: Here we’re extending theUsertype originally defined externally. We have to include both the@keydirective as well as the primary key fields—in this case justUser.id—with the@externaldirective (signifying that this field was originally defined in another service). Thereviewsfield doesn’t have@external, which means it’s being added to theUsertype, and we’ll need to write a resolver for it:
import { ObjectId } from 'mongodb'
import Date from '../../lib/Date'
export default {
...Date,
Query: {
reviews: (_, __, { dataSources }) => dataSources.reviews.all()
},
Review: {
__resolveReference: (reference, { dataSources }) =>
dataSources.reviews.findOneById(ObjectId(reference.id)),
id: review => review._id,
author: review => ({ id: review.authorId }),
createdAt: review => review._id.getTimestamp()
},
User: {
reviews: (user, _, { dataSources }) =>
dataSources.reviews.all({ authorId: ObjectId(user.id) })
}
}These resolvers are taken from our monolith with four additions:
- The
Datecustom scalar resolver. - The
Review.__resolveReferenceresolver, required because this service is the origin of theReviewentity. - The
Review.authorresolver, which returns areference(the same reference passed to__resolveReferenceabove)—an object with an entity’s primary key. The gateway takes this reference and provides it to theUser.__resolveReferenceresolver to get the user object. - The
User.reviewsresolver, which uses the data sourcereview.all()method with a MongoDB selector. Speaking of which, we need aReviewsdata source with a.all()method:
import { MongoDataSource } from 'apollo-datasource-mongodb'
export default class Reviews extends MongoDataSource {
all(query) {
return this.collection.find(query).toArray()
}
}We’ll include this, along with our schema and resolvers, when creating the server:
import { ApolloServer } from 'apollo-server'
import { buildFederatedSchema } from '@apollo/federation'
import resolvers from './resolvers'
import typeDefs from './schema'
import Reviews from './Reviews'
import { mongoClient } from '../../lib/db'
import context from '../../lib/userContext'
const server = new ApolloServer({
schema: buildFederatedSchema([
{
typeDefs,
resolvers
}
]),
dataSources: () => ({
reviews: new Reviews(mongoClient.db().collection('reviews'))
}),
context
})
mongoClient.connect()
server.listen({ port: 4002 }).then(({ url }) => {
console.log(`Reviews service ready at ${url}`)
})We use the same context function as the users service and a new port (4002, versus 4001 for the users service and the default 4000 for the gateway).
One piece of our old schema that we’re missing is Review.fullReview. Since it involves the author’s name, we need to query the users collection. And the service that is responsible for querying the users collection is the users service. So let’s add the field to the users service:
export default gql`
...
extend type Review @key(fields: "id") {
id: ID! @external
fullReview: String!
}
`Like with extend type User, when we extend type Review, we repeat the directive and include the primary key field. However, we have an issue: The fullReview resolver needs data from the review document (authorId, text, and stars). By default, the resolver will only receive an object with the review’s id field.
We can solve this issue with the @requires directive:
export default gql`
...
extend type Review @key(fields: "id") {
id: ID! @external
text: String! @external
stars: Int @external
authorId: ID! @external
fullReview: String! @requires(fields: "authorId text stars")
}
`We list the fields we require in order to resolve fullReview using @requires, and we list those fields above with @external. The last issue is that authorId isn’t currently part of the Review type, so let’s add it to the reviews service schema:
export default gql`
scalar Date
type Review @key(fields: "id") {
id: ID!
text: String!
stars: Int
authorId: ID!
author: User!
createdAt: Date!
updatedAt: Date!
}
...
`This makes authorId appear in the public gateway schema as well, which isn’t ideal, as it unnecessarily clutters the schema, but the ability to define a private, internal field is a planned addition to the federation spec.
Finally, we can implement the fullReview resolver back in the users service:
export default {
...
Review: {
fullReview: async (review, _, { dataSources }) => {
const author = await dataSources.users.findOneById(
ObjectId(review.authorId)
)
return `${author.firstName} ${author.lastName} gave ${review.stars} stars, saying: "${review.text}"`
}
}
}We add the reviews service to our gateway by simply adding it to our serviceList:
const gateway = new ApolloGateway({
serviceList: [
{ name: 'users', url: 'http://localhost:4001/graphql' },
{ name: 'reviews', url: 'http://localhost:4002/graphql' }
],
buildService({ url }) {
return new AuthenticatedDataSource({ url })
}
})We can run both services with:
$ npm run start-servicesAnd in another terminal run the gateway:
$ npm startAnd test! 🙏

✅ Here we see both of the jumps from the reviews service to the users service working: The reviews service resolves Query.reviews and the Review.author reference, and the users service resolves the reference into a user, as well as User.firstName and Review.fullReview.
Next, we can see that going from the users service to the reviews service works. First the users service resolves Query.user, and then the reviews service resolves User.reviews.

To see a more detailed explanation of the query plan—the process by which the gateway determines how to get all the data it needs from the services—we can add this last argument to ApolloGateway():
const gateway = new ApolloGateway({
serviceList...
buildService...
__exposeQueryPlanExperimental: true
})Now inside Playground, we can open the QUERY PLAN tab on the bottom-right:

{
user(id: "5d24f846d2f8635086e55ed3") {
id
firstName
reviews {
stars
text
}
}
}The above query results in the below query plan:
QueryPlan {
Sequence {
Fetch(service: "users") {
{
user(id: "5d24f846d2f8635086e55ed3") {
id
firstName
__typename
}
}
},
Flatten(path: "user") {
Fetch(service: "reviews") {
{
... on User {
__typename
id
}
} =>
{
... on User {
reviews {
stars
text
}
}
}
},
},
},
}Sequence means the following queries are done in sequence—one after the other. So first it does a Fetch from the users service, and then a fetch from the reviews service.
Our first query involves a Parallel in addition to a Sequence:
{
reviews {
author {
firstName
}
fullReview
}
}QueryPlan {
Sequence {
Fetch(service: "reviews") {
{
reviews {
author {
__typename
id
}
__typename
id
authorId
text
stars
}
}
},
Parallel {
Flatten(path: "reviews.@") {
Fetch(service: "users") {
{
... on Review {
__typename
id
authorId
text
stars
}
} =>
{
... on Review {
fullReview
}
}
},
},
Flatten(path: "[email protected]") {
Fetch(service: "users") {
{
... on User {
__typename
id
}
} =>
{
... on User {
firstName
}
}
},
},
},
},
}The gateway first fetches from the reviews service and then does two fetches from the users service for each review, all in parallel.
We can look at the query plan to diagnose performance issues—it’s possible that the query plan will show a lot of fetches in series, which increases latency. A fetch in series—where the second fetch happens after the first is complete—is denoted by Sequence. In the case of bugs, the query plan might also help us discover why the gateway is not working as we expect.
Another tool we have for diagnosing bugs is our gateway’s RemoteGraphQLDataSource, to which we can add the didReceiveResponse method, where we can log responses from the services:
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
willSendRequest...
didReceiveResponse({ response, request, context }) {
console.log('response data:', response.data)
return response
}
}We can also use Apollo Workbench to design our federated data graph before implementing it.
For further reading, here are some federation capabilities we aren’t using:
- Having multiple primary keys or compound primary keys
- Resolving other services’ fields with the
@providesdirective - Modifying the gateway’s response
- Using custom directives