DEV Community

loading...
Cover image for Your First Federated Schema with Apollo

Your First Federated Schema with Apollo

mandiwise profile image Mandi Wise Updated on ・10 min read

The following post is based on the code I demoed during my Apollo Space Camp talk. You can find the recording of that talk here.


GraphQL APIs shine in their ability to expose a single data graph to client applications. What's more, they also make it easy for those clients to query only the data they need and in a way that reflects the relationships between the nodes in the graph.

However, as we express more and more objects and their relationships through types and fields, it doesn't take long for even a moderately complex app to require a large number of type definitions. In turn, it becomes increasingly difficult for multiple people or teams to collaborate on building the API.

When the time comes to move to a distributed GraphQL architecture, Apollo Federation provides a set of tools to split a large schema into smaller implementing services based on separation of concerns, rather than by types alone.

And in contrast to other approaches for creating distributed GraphQL architectures such as schema stitching, Apollo Federation also offers a straightforward, declarative interface to help compose each of the federated schemas back into a single data graph for clients to query.

In this post, I will walk through how to:

  • Transform a regular Apollo Server into one using Apollo Federation
  • Create two implementing services that each manage a portion of the overall schema
  • Extend and reference types across services

Getting Started

Let's begin by setting up a basic Apollo Server. Our first step will be to create a project directory:

mkdir apollo-federation-demo && cd apollo-federation-demo

Next, we'll create a package.json file:

npm init --yes

Now we can install the packages we need to set up our GraphQL API:

npm i apollo-server concurrently graphql json-server node-fetch nodemon

In this demo, we'll spin up a mocked REST API using JSON Server to act as the backing data source for the GraphQL API. The REST API will have /astronauts and /missions endpoints where we can query data about various Apollo-era astronauts and their missions.

To set up a basic Apollo Server with a JSON Server REST API, we'll need to create two files in our project directory:

touch index.js db.json

You can copy and paste this data to use in the new db.json file. Note that the crew field for each mission is an array of IDs that refer to individual astronauts.

In the index.js file, we'll then add the following code:

const { ApolloServer, gql } = require("apollo-server");
const fetch = require("node-fetch");

const port = 4000;
const apiUrl = "http://localhost:3000";

const typeDefs = gql`
  type Astronaut {
    id: ID!
    name: String
  }
  type Query {
    astronaut(id: ID!): Astronaut
    astronauts: [Astronaut]
  }
`;

const resolvers = {
  Query: {
    astronaut(_, { id }) {
      return fetch(`${apiUrl}/astronauts/${id}`).then(res => res.json());
    },
    astronauts() {
      return fetch(`${apiUrl}/astronauts`).then(res => res.json());
    }
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers
});

server.listen({ port }).then(({ url }) => {
  console.log(`Server ready at ${url}`);
});

Our basic GraphQL schema currently contains two queries to fetch all astronauts or a single astronaut by their unique ID (we'll add more queries to get mission data shortly).

To start up our API, we'll need to add a few scripts to the package.json file:

{
  // ...
  "scripts": {
    "server": "concurrently -k npm:server:*",
    "server:rest": "json-server -q db.json",
    "server:graphql": "nodemon index.js"
  },
    // ...
}

With this code in place, we can run npm run server to start up the API. If you like, you can test out one of the astronaut queries now in GraphQL Playground at http://localhost:4000.

Create the Astronauts Service

To refactor our Apollo Server into one using Apollo Federation, we'll start by adding two more packages:

npm i @apollo/federation @apollo/gateway

The @apollo/federation package will allow us to make our services’ schemas federation-ready and @apollo/gateway will help us compose the separate schemas into a single data graph and then distribute incoming GraphQL API requests to underlying services. We still need the apollo-server package installed because we will use an instance of ApolloServer for the gateway API and each of the implementing services we create.

Now we'll create a separate file to manage the astronauts service:

touch astronauts.js

The astronauts.js file will end up looking very similar to what's inside our current index.js file. We'll start by adding the required packages and constants at the top of this file:

const { ApolloServer, gql } = require("apollo-server");
const { buildFederatedSchema } = require("@apollo/federation");
const fetch = require("node-fetch");

const port = 4001;
const apiUrl = "http://localhost:3000";

You'll notice that we import the buildFederatedSchema function from the Apollo Federation package above. This function will allow us to make our astronauts schema federation-ready. We also set the port number to 4001 here because this service will need a dedicated port (and we will continue to use port 4000 for the client-facing gateway API).

Before we move the astronaut-related type definitions into this file, we'll need to familiarize ourselves with the notion of an entity in a federated data graph. An entity is a type that you define canonically in one implementing service and then reference and extend in other services. Entities are the core building blocks of a federated graph and we create them using the @key directive in our schema.

To that end, we'll add a @key directive to the Astronaut type definition when we move the typeDefs and resolvers to astronauts.js. This directive is the way we tell Apollo that Astronaut can be referenced and extended by other services (as long as the other services can identify an astronaut by the value represented by their ID field):

// ...

const typeDefs = gql`
  type Astronaut @key(fields: "id")
    id: ID!
    name: String
  }

  extend type Query {
    astronaut(id: ID!): Astronaut
    astronauts: [Astronaut]
  }
`;

const resolvers = {
  Query: {
    astronaut(_, { id }) {
      return fetch(`${apiUrl}/astronauts/${id}`).then(res => res.json());
    },
    astronauts() {
      return fetch(`${apiUrl}/astronauts`).then(res => res.json());
    }
  }
};

In the code above, you may have also noticed that we use the extend keyword now in front of type Query. The Query and Mutation types originate at the gateway level of the API, so the Apollo documentation says that all implementing services should "extend" these types with any additional operations. The resolvers for the astronauts schema will look exactly as they did our original Apollo Server.

Next, instead of passing the typeDefs and resolvers into the ApolloServer constructor directly, we will instead set a schema option to the return value of calling buildFederatedSchema with the typeDefs and resolvers passed in. We also update the console.log statement so it's clear astronauts service is starting:

// ...

const server = new ApolloServer({
  schema: buildFederatedSchema([{ typeDefs, resolvers }])
});

server.listen({ port }).then(({ url }) => {
  console.log(`Astronauts service ready at ${url}`);
});

Our complete astronauts.js file will now look like this:

const { ApolloServer, gql } = require("apollo-server");
const { buildFederatedSchema } = require("@apollo/federation");
const fetch = require("node-fetch");

const port = 4001;
const apiUrl = "http://localhost:3000";

const typeDefs = gql`
  type Astronaut @key(fields: "id") {
    id: ID!
    name: String
  }
  extend type Query {
    astronaut(id: ID!): Astronaut
    astronauts: [Astronaut]
  }
`;

const resolvers = {
  Query: {
    astronaut(_, { id }) {
      return fetch(`${apiUrl}/astronauts/${id}`).then(res => res.json());
    },
    astronauts() {
      return fetch(`${apiUrl}/astronauts`).then(res => res.json());
    }
  }
};

const server = new ApolloServer({
  schema: buildFederatedSchema([{ typeDefs, resolvers }])
});

server.listen({ port }).then(({ url }) => {
  console.log(`Astronauts service ready at ${url}`);
});

Now we'll need to make some changes in index.js to turn that Apollo Server into the gateway of our GraphQL API. Our refactored index.js file will look like this:

const { ApolloServer } = require("apollo-server");
const { ApolloGateway } = require("@apollo/gateway");

const port = 4000;

const gateway = new ApolloGateway({
  serviceList: [{ name: "astronauts", url: "http://localhost:4001" }]
});

const server = new ApolloServer({
  gateway,
  subscriptions: false
});

server.listen({ port }).then(({ url }) => {
  console.log(`Server ready at ${url}`);
});

To recap, we've made the following updates to index.js:

  • We deleted the gql and fetch imports, as well as the apiUrl, typeDefs, and resolvers constants (because we only need these in astronaut.js now)
  • We instantiated a new ApolloGateway and added the astronauts service to it
  • We updated the ApolloServer constructor by removing the typeDefs and resolvers that were previously passed directly into it, and then we added the gateway to it instead and set the subscriptions option to false because subscription operations are not supported by Apollo Federation at this time

Lastly, before we can start up our new gateway API, we'll need to add a start script for the astronauts service in package.json:

{
  // ...
  "scripts": {
    "server": "concurrently -k npm:server:*",
    "server:rest": "json-server -q db.json",
    "server:astronauts": "nodemon astronauts.js",
    "server:graphql": "nodemon index.js"
  },
  // ...
}

Once again, we can run npm run server and test out our API in GraphQL Playground at http://localhost:4000. We'll know everything is working if the API returns data from the astronaut queries exactly as it did before.

Add a Missions Service

Now that we have our astronauts service up and running, we can create a second implementing service to handle the missions-related data. First, we'll create a missions.js file:

touch missions.js

Next, we'll scaffold the code in missions.js much like the astronauts service:

const { ApolloServer, gql } = require("apollo-server");
const { buildFederatedSchema } = require("@apollo/federation");
const fetch = require("node-fetch");

const port = 4002;
const apiUrl = "<http://localhost:3000>";

const typeDefs = gql``;

const resolvers = {};

const server = new ApolloServer({
  schema: buildFederatedSchema([{ typeDefs, resolvers }])
});

server.listen({ port }).then(({ url }) => {
  console.log(`Missions service ready at ${url}`);
});

We will also define a Mission type, its basic queries, and all of the required resolvers in missions.js:

// ...

const typeDefs = gql`
  type Mission {
    id: ID!
    designation: String!
    startDate: String
    endDate: String
  }

  extend type Query {
    mission(id: ID!): Mission
    missions: [Mission]
  }
`;

const resolvers = {
  Query: {
    mission(_, { id }) {
      return fetch(`${apiUrl}/missions/${id}`).then(res => res.json());
    },
    missions() {
      return fetch(`${apiUrl}/missions`).then(res => res.json());
    }
  }
};

// ...

Now for the fun part! It's time to make a connection between the two different services using the Astronaut entity. Specifically, we're going to add a crew field to the Mission type that returns a list of Astronaut objects.

This is possible thanks to another key federation concept—once an entity is defined in one service we can reference it from other services as needed. To use the Astronaut type with the Mission type's crew field, we'll need to update missions.js as follows:

// ...

const typeDefs = gql`
  type Mission {
    id: ID!
    crew: [Astronaut]
    designation: String!
    startDate: String
    endDate: String
  }

  extend type Astronaut @key(fields: "id") {
    id: ID! @external
  }

  # ...
`;

// ...

In the code above, we include the Astronaut type again so we can use it in this service, but this time we put the extend keyword in front of it. We must also include its key field of id inside the definition and add the @external directive to it to indicate that this field was defined in another service.

Our code won't work quite yet because we still need to create a resolver for the new crew field. When resolving the crew field the only information the missions service will have about the corresponding astronauts is their unique IDs, but that's OK!

To resolve these fields with Apollo Federation, we only need to return an object (or in our case, a list of objects that represent each of the crew members) containing the __typename and the id key field that identifies the astronaut:

// ...

const resolvers = {
  Mission: {
    crew(mission) {
      return mission.crew.map(id => ({ __typename: "Astronaut", id }));
    }
  },
  // ...
};

// ...

The gateway will hand off these representations of the entities to the astronaut's service to be fully resolved, so we also need a way to resolve these references once they reach the originating service. To do that, we must provide a reference resolver for the Astronaut type in astronauts.js to fetch the data for a given entity based in it id key:

// ...

const resolvers = {
  Astronaut: {
    __resolveReference(ref) {
      return fetch(`${apiUrl}/astronauts/${ref.id}`).then(res => res.json());
    }
  },
  // ...
};

// ...

We're now ready to add the missions service to the gateway in index.js:

// ...

const gateway = new ApolloGateway({
  serviceList: [
    { name: "astronauts", url: "http://localhost:4001" },
    { name: "missions", url: "http://localhost:4002" }
  ]
});

// ...

And in package.json, we'll add another start script for the missions service too:

{
  // ...
  "scripts": {
    "server": "concurrently -k npm:server:*",
    "server:rest": "json-server -q db.json",
    "server:astronauts": "nodemon astronauts.js",
    "server:mission": "nodemon missions.js",
    "server:graphql": "nodemon index.js"
  },
  // ...
}

When we run npm run server again, we'll see that we can now query missions with related crew data in GraphQL Playground:

query {
  missions {
    designation
    crew {
      name
    }
  }
}

As a finishing touch, it would great if we could traverse the graph in the other direction too. To that end, we're going to add a missions field to get a list of related Mission objects when querying astronauts.

When adding a missions field to the Astronaut type, we won't need to touch any of our existing code in astronauts.js and we will also get to see a final key federation concept in action. From with the referencing missions service, we can extend the Astronaut type with an additional field.

In missions.js, we'll update our extended Astronaut type:

// ...

const typeDefs = gql`
  # ...

  extend type Astronaut @key(fields: "id") {
    id: ID! @external
    missions: [Mission]
  }

  # ...
`;

// ...

Lastly, we have to resolve the new field from within the missions service too. Inside the missions field resolver, again, we only have access to the data about astronauts that exists within the context of this service. In other words, we only have access to the astronauts' unique IDs.

Due to the limitations of how data can be queried from the mocked REST API, we'll have to settle for fetching all of the mission data and then filtering out the mission objects that don't contain a given astronaut's ID (we're dealing with a small amount of data here, so this will be OK for our demonstration purposes):

// ...

const resolvers = {
  Astronaut: {
    async missions(astronaut) {
      const res = await fetch(`${apiUrl}/missions`);
      const missions = await res.json();

      return missions.filter(({ crew }) =>
        crew.includes(parseInt(astronaut.id))
      );
    }
  },
  // ...
};

// ...

Back over in GraphQL Playground, we can now query an astronaut's with their mission data too:

query {
  astronauts {
    name
    missions {
      designation
    }
  }
}

Conclusion

Congratulations! You just created your first federated data graph using Apollo Federation. We covered a lot of ground in this tutorial, including:

  • How to create a federated data graph, two implementing services, and an Astronaut entity
  • How to reference the Astronaut entity in the missions service and use it for the crew field on the Mission type
  • How to extend the Astronaut entity in the missions service and add a missions field to it so we can traverse these relationships in both directions through the graph

I hope this post has given you a glimpse of how approachable Apollo Federation is if you have a bit of prior experience with Apollo Server. For more details about what's possible with the Apollo Federation and Apollo Gateway packages, be sure to visit the official docs.

You can also find the complete code for this tutorial on GitHub and read more about building full-stack JavaScript applications in my book Advanced GraphQL with Apollo & React.


Photo credit: NASA

Discussion (0)

Forem Open with the Forem app