loading...

Getting Started with Apollo Federation and Gateway

mandiwise profile image Mandi Wise Updated on ・8 min read

Last year, Apollo released an open-source tool called Apollo Federation to help simplify the process of composing multiple GraphQL APIs into a single gateway API.

Having used schema stitching to join GraphQL APIs together in the past, the declarative, no-fuss approach offered by Apollo Federation was a breath of fresh air. In fact, at the time this library was released, I had recently started writing a book about GraphQL and promptly rewrote the first part to use Apollo Federation instead.

Having spent the last 10 months exploring this library, I thought I would write a series of blog posts here to share some tips about what I've learned along the way.

In the first post, I'll provide a brief overview of how to set up two "federated schemas" in separate services using Apollo Federation and then combine them into a single GraphQL API using Apollo Gateway. I'll also share my preferred approach for setting up npm scripts to start and reload the gateway API and the two implementing services.

TL;DR You can find the the complete code here.

Our first step will be to create a project directory:

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

Then we'll run npm init in the new directory (the --yes flag creates the package.json file without asking any questions):

npm init --yes

Next, we'll install all of the packages we need:

npm i apollo-server@2.14.2 @apollo/federation@0.13.2 @apollo/gateway@0.13.2 \
esm@3.2.25 graphql@15.0.0 nodemon@2.0.2 concurrently@5.1.0 wait-on@4.0.1

Here's an explanation of what the above packages will be used for:

  • apollo-server: We'll need an instance of ApolloServer for the gateway API and each of the services we create.
  • @apollo/federation: This package will allow us to make our services' schemas composable.
  • @apollo/gateway: This package will distribute incoming GraphQL API requests to underlying services.
  • graphql: Apollo requires this library as a peer dependency.
  • esm: This package is a "babel-less, bundle-less ECMAScript module loader" that will allow us to use import and export in Node.js without any hassle.
  • nodemon: Nodemon will automatically reload our application when files change in the project directory.
  • concurrently: We can run multiple commands at the same time using this package. It also has support for shortened commands with wildcards.
  • wait-on: It would be a good idea to wait and make sure that the implementing services' ports are available before starting the gateway API, so we'll use this package for that.

Next, we'll need to create some directories and files to organize our project. To set the scene (no pun intended 🙃), we're going to create a mini IMDB-like API that provides data about films, actors, and directors. The beauty of Apollo Federation is that it allows you to split an API based on separation of concerns rather than being limited to breaking up a GraphQL API by types.

In practice, that means we can define a type in one service's schema and access it or even extend it with additional fields in another. This feature makes it much easier to split an API logically by product area.

We'll manage access to our data via separate "films" and "people" services. Each service will have a federated schema, and we will merge those two schemas into the gateway-level API so that clients can query data from a single API without any direct concern for the two underlying services.

Let's add a directory for each of the services now:

mkdir films people

We'll also add index.js files to contain the code for the two services and the gateway:

touch index.js films/index.js people/index.js

Lastly, we'll need some mock data to query via the API. Add a data.js file too:

touch data.js

And add this code to it:

export const people = [
  { id: "1", name: "Steven Spielberg" },
  { id: "2", name: "Richard Dreyfuss" },
  { id: "3", name: "Harrison Ford" },
];

export const films = [
  {
    id: "1",
    title: "Jaws",
    actors: ["2"],
    director: "1",
  },
  {
    id: "2",
    title: "Close Encounters of the Third Kind",
    actors: ["2"],
    director: "1",
  },
  {
    id: "3",
    title: "Raiders of the Lost Ark",
    actors: ["3"],
    director: "1",
  },
];

We'll start by setting up the people service. Add the following code to people/index.js:

import { ApolloServer, gql } from "apollo-server";
import { buildFederatedSchema } from "@apollo/federation";

import { people } from "../data.js";

const port = 4001;

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

  extend type Query {
    person(id: ID!): Person
    people: [Person]
  }
`;

Above, we have a basic set of type definitions to describe a Person in our API. A Person can be either an actor or a director, but we'll leave it up to the films service to make that distinction later. You'll see a @key directive has been added to the Person type definition—this special directive makes Person an entity and it's the way we tell Apollo that this type can be referenced and extended by other services (as long as the other services can identify a person by the value represented by their id field).

There are two other things to note in this file. The first is that we import buildFederatedSchema from @apollo/federation so we can later make our schema federation-ready. The second is that we use the extend keyword in front of type Query because the Query and Mutation types originate at the gateway level so the Apollo documentation says that all implementing services should extend these types with any additional operations.

Next, we'll add some resolvers for the types in people/index.js:

// ...

const resolvers = {
  Person: {
    __resolveReference(object) {
      return people.find((person) => person.id === object.id);
    }
  },
  Query: {
    person(_, { id }) {
      return people.find((person) => person.id === id);
    },
    people() {
      return people;
    }
  }
};

The resolvers for Query are what we would expect to see, but we encounter something interesting under Person with __referenceResolver. This reference resolver is how we explain to the gateway to fetch a person entity by its @key field (which is the id) when referenced by other services.

Lastly, we'll kick off a new ApolloServer for this service at the bottom of people/index.js, using the return value of buildFederatedSchema for the schema option in the server, rather than passing in the typeDefs and resolvers explicitly:

// ...

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

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

This is all the code we need for our people service. Before we turn our attention to the films service, we'll set up the gateway API in index.js:

import { ApolloGateway } from "@apollo/gateway";
import { ApolloServer } from "apollo-server";

const port = 4000;

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

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

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

At the gateway level we once again instantiate an ApolloServer, but this time we have also imported and instantiated an ApolloGateway and passed that into the ApolloServer instead of a schema. The ApolloGateway constructor is passed a serviceList array of objects where each object describes one of the federated schemas we want to compose in the gateway. Lastly, we set subscriptions to false in this ApolloServer because Apollo Gateway does not support subscriptions at this time.

With our current code in place, we can start up our GraphQL API for the first time. To do that, we'll need to update scripts in package.json by creating three new scripts. We create a dev:people script to start up the people service with nodemon and a dev:gateway script that waits for the people service's port to be available, then starts the gateway API up using nodemon too. Finally, we create a dev script that uses concurrently to start up all dev:- scripts using a wildcard:

{
  ...
  "scripts": {
    "dev": "concurrently -k npm:dev:*",
    "dev:people": "nodemon -r esm ./people/index.js",
    "dev:gateway": "wait-on tcp:4001 && nodemon -r esm ./index.js"
  },
  ...
}

Note that we use the -r (or --require) flag for both the people service and gateway API processes to preload the esm module when running Node.js (as is required by the esm package).

Try running npm run dev now to make sure that the GraphQL API is available at http://localhost:4000/graphql. You will be able to open GraphQL Playground at this URL in your browser now too.

Next, we'll build out the films service. The schema for the films service will be more involved than the people service because, in addition to adding a Film type, it will both reference and extend the previously created Person type. First, we'll set up the imports and type definitions in films/index.js:

import { ApolloServer, gql } from "apollo-server";
import { buildFederatedSchema } from "@apollo/federation";

import { films } from "../data.js";

const port = 4002;

const typeDefs = gql`
  type Film {
    id: ID!
    title: String
    actors: [Person]
    director: Person
  }

  extend type Person @key(fields: "id") {
    id: ID! @external
    appearedIn: [Film]
    directed: [Film]
  }

  extend type Query {
    film(id: ID!): Film
    films: [Film]
  }
`;

To use the Person type in the film service we have to define it again, but this time we put the extend keyword in front of it. We also have to include its key field of id, but use this time add the @external directive to indicate that it was defined in another service. After that, we add two new fields to the Person type so we can list what films the person appeared in or directed.

In the Film type, we can also use the Person object to list people who acted in or directed the film, but this time in relation to that film. By both referencing and extending the Person type in the films service, the relationships between people and films can be traversed in both directions in the data graph even though their corresponding types are defined in different services.

Next, we'll need to write resolvers for all of the new types and extra fields added by the films service. Add the following code to films/index.js:

// ...

const resolvers = {
  Film: {
    actors(film) {
      return film.actors.map((actor) => ({ __typename: "Person", id: actor }));
    },
    director(film) {
      return { __typename: "Person", id: film.director };
    }
  },
  Person: {
    appearedIn(person) {
      return films.filter((film) =>
        film.actors.find((actor) => actor === person.id)
      );
    },
    directed(person) {
      return films.filter((film) => film.director === person.id);
    }
  },
  Query: {
    film(_, { id }) {
      return films.find((film) => film.id === id);
    },
    films() {
      return films;
    }
  }
};

When resolving the actors and directors fields on Film the only information the film service has about those people is their unique ID, but that's OK! To resolve these fields with Apollo Federation, we only need to return an object (or list of objects) containing the __typename and the key field/value to identify that object when the request is forwarded to the people service.

Additionally, even though the Person type is initially defined by another service, we need to resolve the new fields that the films service adds here by matching the person's ID to any films where their ID matches the director ID or appears in the actors array.

The final piece of code to add in films/index.js starts the ApolloServer for this service, just as we did in the people service:

// ...

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

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

We'll need to add the films service to the gateway API now in index.js:

// ...

const gateway = new ApolloGateway({
  serviceList: [
    { name: "people", url: "http://localhost:4001" },
    { name: "films", url: "http://localhost:4002" } // NEW!
  ]
});

// ...

Lastly, we'll add another npm script in package.json to start the films service and also require the gateway to wait for the films service's port now:

{
  ...
  "scripts": {
    "dev": "concurrently -k npm:dev:*",
    "dev:people": "nodemon -r esm ./people/index.js",
    "dev:films": "nodemon -r esm ./films/index.js",
    "dev:gateway": "wait-on tcp:4001 tcp:4002 && nodemon -r esm ./index.js"
  },
  ...
}

Our GraphQL API is now ready to go—try out some queries in GraphQL Playground to make sure you can query people and films as expected. You can see the final version of the code here as a reference.

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. Thanks for coding along!

Posted on Jun 9 by:

mandiwise profile

Mandi Wise

@mandiwise

JavaScript Developer 👩‍💻 Machine Learning Enthusiast 🤖 Author of Advanced GraphQL with Apollo & React 🚀

Discussion

markdown guide
 

Thank you for the article. Is it possible for a gateway service A to have gateway service B as an item in gateway service A's service list? In other words, can we nest gateway services?

 

Out of the box, I think this would be contrary to the goals of Apollo Federation given that an implementing service is intended to manage part of a graph, not an entire graph. You could try wrapping gateway B in an implementing service in gateway A, but you would need to redefine the parts of the schema you want to expose in wrapper service's type defs and resolvers. I've done this with portions of the GitHub GraphQL API inside of an implementing service before.