DEV Community

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

touch data.js
Enter fullscreen mode Exit fullscreen mode

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",
  },
];
Enter fullscreen mode Exit fullscreen mode

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]
  }
`;
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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"
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

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]
  }
`;
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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!
  ]
});

// ...
Enter fullscreen mode Exit fullscreen mode

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"
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (13)

pic
Editor guide
Collapse
kieronjmckenna profile image
kieronjmckenna • Edited

Hi Mandi,

I've been using this set-up for a few weeks now, does your gateway application crash when nodemon restarts one of your services?

Apollo throws this error "This data graph is missing a valid configuration. Couldn't load service definitions for "accounts" at localhost:4001: request to localhost:4001/ failed, reason: connect ECONNREFUSED 127.0.0.1:4001" when ever nodemon restarts one of my servers.

I'm not confused as to why this is happening as obviously the gateway server is dependant on the services. Have you encountered this and if so do you have a work around?

Collapse
mandiwise profile image
Mandi Wise Author

Automatic reloading of the gateway can be an issue, even with wait-on in place. You may want to look at some experimental features of Apollo Gateway (github.com/apollographql/federatio...) to help facilitate this, or look into managed federation instead.

Collapse
kieronjmckenna profile image
kieronjmckenna

Easy solution to the development problem I referenced is to use wait-on in index.js before building the gateway rather than in the command line.

This way when nodemon restarts the gateway server it waits for the other services on every restart, not just the first time we run the command.

Thought I'd mention this for anyone who ran into the same problem.

Collapse
robdwaller profile image
Rob Waller

Nice article, I have a question.

What happens if I have two services which persist the same type? Is there a way to manage this? 😂🤦‍♂️

My understanding from your article and other reading is types should be persisted by a singular service?

Collapse
mandiwise profile image
Mandi Wise Author

You can definitely do this Apollo Federation, but how you handle it will depend on what kind of type you're dealing with (e.g. object, interface, custom scalar...). The Apollo docs have a good explanation of how to deal with each different type: apollographql.com/docs/apollo-serv...

Collapse
robdwaller profile image
Rob Waller

Ok thanks will give that a read. 👍

Collapse
dmahajan1609 profile image
dmahajan1609

Hi Mandi,

I've been trying out Apollo Federation for a week now and your samples/demos have helped get a project and running so thank you! I had a couple use-cases that seem like aren't currently supported unless I'm missing to understand something.

I'll try to explain my problem using your example above.
Given the Film type,

  • How would we resolve an array of actors with a Film.id? This is with the assumption that we don't have access to the actors.id array within the Films dataset and need to use Film types primary key.
  • How would we resolve nested Objects of one entity from an extended entity? So in the following example, How can I resolve Film.person.phone (without calls to all APIs in the full chain of the Person: { _resolveReference()})? Thanks in advance!


const typeDefs = gql

type Person @key(fields: "pid") {
pid: ID!
firstName: String
lastName: String
address: String
contact: [Phone] <--- separate API call
hobbies: [Interest] <--- separate API call
}

type Phone @key (fields: "pid") {
pid: ID!
number: String
type: String
}

type Interest @key (fields: "pid") {
pid: ID!
name: String
type: String
}

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

Collapse
pmirandaarias profile image
Paulo Miranda

Very nice and helpfull article. A question Mandi, it's mandatory to use apollo/gateway? I have a monorepo project with Lerna with different packages, and I want to use gql files from different packages, but not all of them will run as a services with endpoints

Collapse
mandiwise profile image
Mandi Wise Author

I'm not sure I have a clear understanding of the question, but you do need to use @apollo/gateway to declaratively compose your implementing services together. However, you don't need to use @apollo/federation to create your implementing services. Apollo lists third-party support for creating federated GraphQL schemas here: apollographql.com/docs/apollo-serv...

Collapse
mrkotov profile image
Georgi Kotov

Hey Mandi,

First of all great article, many thanks!

I have a question around the Gateway. My use case is that I want to create a Federated Graph via the Gateway. The GraphQL endpoints in the gateway are not managed by us they could be provided by third party companies.

So does Gateway does some magic or expect something specific on the discovery side of things:

  1. Introspects each of the GraphQL APIs (given introspection is enabled on them) and build schemas on its own. (This is more of a generic Apollo server question maybe)
  2. Should the underlying GraphQL APIs be apollo based themselves or they can be like Microsoft Graph or Facebook Graph APIs?
  3. Should you always have the schema definition of each service in the list in case you will be building the federated graph?
  4. Should you handle at some middleware level the Authentication for the different services which are agnostic of each other's scope/roles?

Sorry for the many questions, and thanks in advance.

Collapse
tomschreck profile image
Tom Schreck

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?

Collapse
mandiwise profile image
Mandi Wise Author

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.

Collapse
moatazabdalmageed profile image
Moataz Mohammady

Many thanks @mandi for this tutorial

I'm going to clone the repo and play with films !

thanks