Logo

dev-resources.site

for different kinds of informations.

Apollo Federation with Local Subgraphs in a Single Node Process

Published at
12/12/2023
Categories
graphql
apollo
federation
node
Author
lukasjapan
Categories
4 categories in total
graphql
open
apollo
open
federation
open
node
open
Author
10 person written this
lukasjapan
open
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.

apollo Article's
30 articles in total
Favicon
A Beginner’s Guide to Building GraphQL APIs with Apollo Server
Favicon
Apollo Client in 6 minutes with ReactJS
Favicon
Automatically Generated GraphQL Middleware Service
Favicon
GraphQL Federation with Ballerina and Apollo - Part II
Favicon
GraphQL Federation with Ballerina and Apollo - Part I
Favicon
Cookies Setup in Apollo Studio for NodeJS GraphQL Servers
Favicon
Understanding Apollo Client Cache: How to Manage and Update Nested Data Structures Effectively
Favicon
All you need to know about Apollo-Angular Queries
Favicon
GraphQL Code Generator with TypeScript, React and Apollo Client
Favicon
Apollo GraphQL products for dummies
Favicon
Apollo Federation with Local Subgraphs in a Single Node Process
Favicon
GraphQL in Your Ember App with Glimmer Apollo
Favicon
How to solve the N plus 1 problem in GraphQL with Prisma and Apollo
Favicon
Help! Apollo is changing my graphql query response
Favicon
Handle Multiple Queries & Mutations in React Form using Apollo Client
Favicon
Hasura vs Apollo: Comparing GraphQL Platforms
Favicon
Revolutionize Your Next.js State Management with React Button OnClick and Apollo Set Up
Favicon
Apollo integration with MeteorJS v4.2 release and looking to v5
Favicon
The Ultimate GraphQL and Apollo Server Course with Express and TypeScript
Favicon
Working with the Apollo Client in Faust.js
Favicon
Using Postman and Postman Interceptor to authenticate a session cookie based GraphQL API
Favicon
React.Js, GraphQL and Apollo client
Favicon
How we migrated to Apollo Server 4
Favicon
Take your Next js + GraphQL + TypeScript Setup to the next level
Favicon
Pagination in Headless WordPress with WPGraphQL, Apollo, & Next.js
Favicon
Apollo Server Errors for Clients with TypeScript
Favicon
GraphQL and Apollo Server for Beginners: A Practical Guide
Favicon
Appwrite vs. Hasura vs. Apollo
Favicon
GraphQL no front-end (React e Apollo)
Favicon
How to use apollo client cache for local state management

Featured ones: