DEV Community

Lukas
Lukas

Posted on • Edited on

Apollo Federation with Local Subgraphs in a Single Node Process

In the world of GraphQL, Apollo Federation is a method used to standardize the process of stitching multiple graphs together. At the AI health-tech startup Ubie, we are currently exploring the idea of transitioning from a traditional stitching approach to Apollo Federation. However, many tutorials assume the need to set up multiple applications and processes, which can make it challenging to test and verify the technology stack effectively. This article will explore how to implement the entire Apollo Federation tech stack within a single node process. This approach keeps the boilerplate code minimal, simplifies the testing process, and provides a better understanding of Apollo Federation.

Let's get started.

GraphQL Federation

In Apollo Federation, the graphs that will be stitched together are referred to as "Subgraph"s, and the resulting graph is called the "Supergraph".

Each Subgraph is a regular GraphQL graph supplemented with custom directives provided by the Apollo Federation project. These directives, which define the stitching rules, allow all Subgraphs to be joined together automatically to form the Supergraph. The Supergraph includes all types and fields from the Subgraphs.

Apollo Federation can be seen as an opinionated approach to stitch/merge multiple GraphQL resources because we have to operate within the boundary of the provided directives.

Defining the Subgraphs and the Supergraph

When I started learning GraphQL, I considered it an alternative to a RESTful API (over HTTP). However, GraphQL is more than just a replacement for such an API; instead, it is a query language for the data you want to provide. GraphQL defines how you model and access your data but is not necessarily tied to a specific transport layer. This means we can also retrieve our data within a process via function calls.

But first, let's start defining our data models. Consider the following Subgraphs:

# subgraph1.graphqls

enum PetType {
  CAT
  DOG
}

type Pet @key(fields: "id") {
  id: ID!
  type: PetType!
  name: String!
}

type Query {
  pets: [Pet!]!
  pet(id: ID!): Pet
}
Enter fullscreen mode Exit fullscreen mode
# subgraph2.graphqls

type Pet @key(fields: "id") {
  id: ID!
  price: Int!
}

type Store {
  id: ID!
  name: String!
  address: String!
  pets: [Pet!]!
}

type Query {
  petstores: [Store!]!
}
Enter fullscreen mode Exit fullscreen mode

In this example, Subgraph1 manages the definition of pets, while Subgraph2 will focus on managing pet stores and selling the pets. @key is a directive specific to Apollo Federation. I won't go into detail about explaining the directives here but instead focus on how to get everything running.

To generate the Supergraph, we use Apollo's rover cli. This command can be used to compose (stitch) all Subgraphs into the Supergraph which is then represented by a new single schema file. For Rover CLI, we have to specify all Subgraph locations with a configuration file.

# supergraph.yaml
federation_version: =2.5.7
subgraphs:
  subgraph1:
    routing_url: http://subgraph1
    schema:
      file: ./subgraph1.graphqls
  subgraph2:
    routing_url: http://subgraph2
    schema:
      file: ./subgraph2.graphqls
Enter fullscreen mode Exit fullscreen mode

Note that arbitrary values have been used for routing_url of each Subgraph. These values will be used to implement internal routing later.

You can run the following command to compose the Supergraph schema file:

rover supergraph compose --elv2-license accept --config ./supergraph.yaml --output ./supergraph.graphqls
Enter fullscreen mode Exit fullscreen mode

I recommend watching for changes in all Subgraph schema files with the help of nodemon and then re-running the above command to ease development. Furthermore, when using Typescript, we can then watch for changes in the Supergraph schema file with typescript-codegen and generate type definitions on the fly.

Resulting Supergraph:

schema
  @link(url: "https://specs.apollo.dev/link/v1.0")
  @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
{
  query: Query
}

directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE

directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION

directive @join__graph(name: String!, url: String!) on ENUM_VALUE

directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE

directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION

directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA

scalar join__FieldSet

enum join__Graph {
  SUBGRAPH1 @join__graph(name: "subgraph1", url: "http://subgraph1")
  SUBGRAPH2 @join__graph(name: "subgraph2", url: "http://subgraph2")
}

scalar link__Import

enum link__Purpose {
  """
  `SECURITY` features provide metadata necessary to securely resolve fields.
  """
  SECURITY

  """
  `EXECUTION` features provide metadata necessary for operation execution.
  """
  EXECUTION
}

type Pet
  @join__type(graph: SUBGRAPH1, key: "id")
  @join__type(graph: SUBGRAPH2, key: "id")
{
  id: ID!
  type: PetType! @join__field(graph: SUBGRAPH1)
  name: String! @join__field(graph: SUBGRAPH1)
  price: Int! @join__field(graph: SUBGRAPH2)
}

enum PetType
  @join__type(graph: SUBGRAPH1)
{
  CAT @join__enumValue(graph: SUBGRAPH1)
  DOG @join__enumValue(graph: SUBGRAPH1)
}

type Query
  @join__type(graph: SUBGRAPH1)
  @join__type(graph: SUBGRAPH2)
{
  pets: [Pet!]! @join__field(graph: SUBGRAPH1)
  pet(id: ID!): Pet @join__field(graph: SUBGRAPH1)
  petstores: [Store!]! @join__field(graph: SUBGRAPH2)
}

type Store
  @join__type(graph: SUBGRAPH2)
{
  id: ID!
  name: String!
  address: String!
  pets: [Pet!]!
}
Enter fullscreen mode Exit fullscreen mode

As you can see, a lot of new (Apollo Federation) directives have been added.

Running the Gateway

The Apollo Router / Gateway can process GraphQL requests against the Supergraph. After receiving a request from the user, it will break it into multiple requests for the Subgraphs. The actual data fetching and resolving logic still lies within the responsibilities of the subgraphs. The router knows which endpoints need to be accessed on runtime because the Subgraph endpoints (or routing URLs; our arbitrary values) are embedded within the Supergraph schema as custom directives. The results from the Subgraphs are fetched, combined, and returned to the client as a single response by the router.

Besides a JavaScript library, Apollo also provides a precompiled binary for this task, but since we want to keep it simple, and more importantly, want to use it inside in a single (node) process, we have to use the library.

First, we implement the subgraphs:

// subgraph1.ts / subgraph2.ts / ...
import { buildSubgraphSchema } from "@apollo/subgraph";
import { LocalGraphQLDataSource } from "@apollo/gateway";
import { gql } from "graphql-tag";

const schema = readFileSync("./subgraph1.graphqls", "utf8");
const resolvers = {
  Query: {
    // define resolvers here
  },
  Pet: {
    // define resolvers here
  },
};

const graphqlSchema = buildSubgraphSchema({ typeDefs: gql(schema), resolvers });
export const subgraph1 = new LocalGraphQLDataSource(graphqlSchema);
Enter fullscreen mode Exit fullscreen mode

And then glue it together:

// supergraph.ts
import { ApolloServer } from "@apollo/server";
import { ApolloGateway, RemoteGraphQLDataSource } from "@apollo/gateway";
import { subgraph1 } from "./subgraph1";
import { subgraph2 } from "./subgraph2";

const gateway = new ApolloGateway({
  supergraphSdl: readFileSync("./supergraph.graphqls", "utf8"),
  buildService: ({ url }) => {
    if (url === "http://subgraph1") {
      return subgraph1;
    } else if (url === "http://subgraph2") {
      return subgraph2;
    } else {
      return new RemoteGraphQLDataSource({ url });
    }
  },
});
const server = new ApolloServer({ gateway });

// expose graph via http on port 4003
const config = { listen: { port: 4003 } };
const { url } = await startStandaloneServer(server, config);
console.log(`🚀 Ready at ${url}`);
Enter fullscreen mode Exit fullscreen mode

Internal routing is achieved by providing the buildService configuration option, checking against the routing URLs, and returning a LocalGraphQLDataSource instance if we encounter one of the previously defined arbitrary values. This will tell the library to execute locally defined resolvers instead of routing over HTTP requests.

Finally, the Supergraph is exposed to the client over HTTP by using the startStandaloneServer function. However, requests from the Gateway/Router to the Subgraphs are all handled internally.

Using the Graph "Backend for Frontend" style

Sometimes, the graph is too complex for a single client or should remain private. The requests to the Gateway can be processed in code as well by using the executeOperation method of the ApolloServer instance. The example below shows how to do this in a request handler from the express framework.

// ...
const server = new ApolloServer({ gateway });
await server.start();

// http server
const app = express();
// uncomment the following line to expose the graph
// app.use("/graphql", cors(), express.json(), expressMiddleware(server));

app.get("/", async (_, res: Response) => {
  const query = readFileSync("./query.graphql", "utf8");
  const variables = { key: "arg" }
  const result = await server.executeOperation({ query, variables });
  const html = `process ${result} here`
  res.send(html);
});

app.listen(80, () => {
  console.log(`Server running on http://localhost/`);
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

There we have it. With the above techniques, GraphQL with Apollo Federation is used solely as an API interface, and we never used the transport layer. We tested and verified the complete architecture within a single Node application. Arranging and transforming the components (Subgraphs/Gateway) into separate processes should be straightforward if scaling requirements demand it.

For a more sophisticated example and some working code, check out the GitHub repository accompanying this article.

GraphQL at Ubie

At the global division in Ubie, we primarily work on a TypeScript and GraphQL stack. We had the opportunity to redesign most system components, maintaining a clean codebase. If you are enthusiastic about these technologies, why don't you check out our recruitment page and learn more about possible opportunities? Maybe we will be able to work together in the future.

Top comments (0)