patrick.wtf

📝

Using Apollo Federation with local schemas

Using Apollo Federation with local schemas

Recently I needed to create a proxy to a third party API in our GraphQL Federation schema but didn’t want to spin up a new service just for this. This blog posts explains how to use a local schemas inside the Apollo Federation Gateway.

A bit of context

At work we use Prismic to power the content of our site (at least most of it). We are currently working on a new project that is using Apollo Federation as our Gateway[^1].

We wanted to add the Prismic API to our Gateway, so to use only one API Jon our frontend. Unfortunately when a GraphQL API doesn’t provide support for Apollo Federation things aren’t as easy as specifying the service in Apollo’s services list.

A basic Federation example

Let’s supposed that we have one Apollo federation that’s consuming one service for the time being.

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

const server = new ApolloServer({ gateway });

server.listen();

As you can see the ApolloGateway requires a list of GraphQL services as objects with a name and a url. This is a bit unfortunate as this means that using a local schema doesn’t work by default.

Note: we can’t use Prismic’s GraphQL API directly as they don’t provide the necessary fields for federation. In addition to that they also use GET requests instead of POST requests for the API call, and ApolloGateway always sends POST requests.

Note: we won’t be using Prismic API in the examples of this blog post, as I wanted to keep the code simpler sine Prismic API needs a bit of work to fetch the ref version, which is not really useful for this example. I’ll be happy to provide an example repo with Prismic if it’s needed. Drop me a tweet in that case!

What can we do to make this work?

So, what makes Federation work with multiple service is the Apollo Gateway, which basically acts as glue between the services. It knows how to compose the schemas and how to create a query plan for executing a query.

From my understanding of the source code, Apollo Gateway seems to support local services, but, as mentioned in this GitHub Issue it doesn’t allow to support both local and remote services at the same time by default.

Fortunately for us, we can use buildService to provide create a custom DataSource for our remote schema.

DataSource is what Apollo Gateway uses to fetch data from a service. Which means we can create a DataSource that proxies the requests to Prismic.

We will be using Trevor Blades’ Countries API, a public GraphQL API that doesn’t support Apollo Federation.

So, buildService is called for each service specified in the serviceList configuration parameter for Apollo Gateway. Let’s go and update that to have a service for the countries service:

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

const server = new ApolloServer({ gateway });

server.listen();

The only caveat that we have is that we have need to provide a url for our service, even it is fake.

The next step is to provide a buildService function, which looks like this:

const gateway = new ApolloGateway({
  serviceList: [
    { name: "products", url: "http://localhost:4002" },
    { name: "countries", url: "http://countries" },
  ],

  buildService: ({ url }) => {
    if (url === "http://countries") {
      return new LocalGraphQLDataSource(getCountriesSchema());
    } else {
      return new RemoteGraphQLDataSource({
        url,
      });
    }
  },
});

const server = new ApolloServer({ gateway });

server.listen();

Ok, the last step is to create the function that will return the schema for the countries service. Similar to RemoteGraphQLDataSource Apollo Gateway also provides a LocalGraphQLDataSource which accepts a schema as parameter instead of a url. This means that we can provide our own schema!

GraphQL Tools

So now we know we can provide our own schema to Apollo Gateway, but how can we proxy an existing schema? We could do this manually, but it would take a bit of time, luckily for us there’s a package called graphql-tools that allows to use a remote schema as if it were local.

In addition to that we also need to use graphql-transform-federation to provide the required fields for Apollo Federation. Let’s see how our function looks like:

const executor = async ({ document, variables }) => {
  const query = print(document);
  const fetchResult = await fetch("https://countries.trevorblades.com/", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ query, variables }),
  });
  return fetchResult.json();
};

const getCountriesSchema = async () => {
  const schema = wrapSchema({
    schema: await introspectSchema(executor),
    executor,
  });

  return transformSchemaFederation(schema, {
    Query: {
      extend: true,
    },
  });
};

There’s a few things going on there, first we have a function called executor, this is our core function that proxies the calls to the countries service. This is not doing anything special, it is getting the document and variables and sending them to the countries services. For Prismic we’d add headers for authentication and change the request to be a GET. But the rest would be pretty much the same.

Finally, the last piece of the puzzle, getCountriesSchema, which uses the utilities from graphql-tools and graphql-transform-federation to create a proxy schema with support for Federation.

getCountriesSchema uses wrapSchema to create a schema based on our the Countries GraphQL API. We need to pass a schema, an executor and some optional transforms. We are not use any transform here, but one use case is to rename the types so they don’t conflict with other services.

Finally it calls transformSchemaFederation to obtain a Federation compatible schema, the only option we are passing here is telling Federation that this schema is extending the Query type, so all the fields provided here will be composed into the federated schema. We can also pass more options here, but at the moment we didn’t need that.

Note: the code in the example is a bit broken as we are not awaiting getCountriesSchema in our buildServices, the final code provided in the git repo has the correct solution, I wanted to keep the code example simple here.

Conclusion

As you can see you need a little bit of code to proxy a non federated GraphQL service with Apollo Gateway, but it is not too bad and I think it is pretty cool that we can do this as we can move the code that deals with multiple schemas from our clients to our gateway, simplifying how they fetch data.

Also graphql-transform-federation allows to specify a Federation configuration so it is even possible to use Apollo Federation features to extend types with a non federated service, which can definitely make the schema easier to use.

To see a fully working example of this head to the example repo I’ve written for this blog post.

Note: all of this post might be deprecated in future when project constellation gets released to the public, which should allow federation of service that don’t have the (currently) required fields. In the meantime, feel free to try this approach.

[^1]: This is part of a bigger rewrite of our internal API, more on that in future blog posts hopefully.

Webmentions