Skip to main content

Understand and Build a GraphQL Federated API Gateway with Apollo and Nest

Greetings

GraphQL revolutionized modern API development by addressing issues in REST through a unified interface. While GraphQL is well-suited for building monolithic services, modern applications are often complex and developed using the microservices architecture. How can we harness GraphQL to provide a unified API in a microservices application? This is where GraphQL federation comes in to solve the problem.

Complete source code - Nest GraphQL Federation

Challenges faced in REST API

Before delving into the GraphQL gateway, it's important to first grasp the issues associated with the REST gateway. In the example below, when we need to retrieve a book along with its author, the client must initially fetch the book and then make an additional request to obtain the author's information. Alternatively, in the case of multiple BFFs (Backend For Frontend), we would need to implement this in the API gateway. However, this would necessitate the maintenance of multiple BFFs.



We can implement a GraphQL server in the same way, but that's not the primary purpose of using GraphQL. Its primary purpose is to provide a unified gateway.

GraphQL Federation

Apollo addressed this by introducing the Apollo Federation. Microservices are developed as subgraphs independently as usual, and the gateway acts as the supergraph by collecting all the subgraphs and providing a unified graph. This significantly simplifies the Backend For Frontend (BFF) and eliminates the need for multiple BFFs.

But, does it solve the original issue?

The primary issue we discussed is that the client has to send multiple calls to request data. A Federated supergraph eliminates this by aggregating data through requests to relevant microservices. This is the beauty of the federated graph.

query book {
  book(id: 1) {
    id
    title
    author {
      id
      firstName
      lastName
    }
  }
}
Here, we query the book and author in a single query. The Supergraph then resolves all the relations and returns the requested response to the client, in a single API call!!

Let's delve deeper into this by constructing an actual schema. Since our emphasis is on federation, I assume you already have a fundamental understanding of GraphQL.

Configuration steps

  • Configure subgraph
    • Install federation dependencies
    • Federated subgraph schema
    • Modify resolvers
    • Module configuration
  • Configure supergraph
    • Install federation supergraph dependencies
    • Configure module
    • Define subgraphs

Configure subgraph for federation

Let's first configure the author's service as a federated subgraph.
npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql
npm i --save @apollo/federation @apollo/subgraph
In the Apollo Federation version 2, a subgraph schema is more akin to a regular schema. However, we need to define key identification for the supergraph to resolve subgraphs when necessary.

Configure subgraph module

We need to define the subgraphs module as a federated module and use the federation driver.
import { Module } from '@nestjs/common';
import { AuthorsModule } from './authors/authors.module';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloFederationDriver, ApolloFederationDriverConfig } from '@nestjs/apollo';
import { join } from 'path';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      autoSchemaFile: {
        federation: 2,
        path: join(process.cwd(), 'src/authors-schema.gql'),
      },
    }),
    AuthorsModule
  ],
})
export class AppModule {}

Configure subgraph entity

To define the entity for generating the subgraph schema, Nest provides decorators as follows.
import { ObjectType, Field, ID, Directive } from '@nestjs/graphql';

@ObjectType()
@Directive('@key(fields: "id")')
export class Author {

  @Field(() => ID!)
  id: number;

  @Field(() => String)
  firstName: string;

  @Field(() => String)
  lastName: string;
}

Configure subgraph resolver

This is similar to a regular resolver. One key addition is that we need to define a resolver method for resolving authors when called by the supergraph. We will see the practical usage of this when resolving books with authors.
  @ResolveReference()
  resolveReference(reference: { __typename: string; id: number }): Author {
    return this.authorsService.findOne(reference.id);
  }
Now, if you run this as a standalone GraphQL server, it will work fine.
Note that the book subgraph is the same as the author graph. Therefore, I have omitted the code for the book graph.

Configure the Supergraph Gateway

The Supergraph is the client-facing gateway that routes requests to downstream subgraphs and resolves dependencies as needed. It's another microservice with a dedicated responsibility. We create a general microservice and configure the Supergraph gateway and downstream services.
npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql
npm install --save @apollo/gateway

Configure Gateway Module

Here, we also need to configure the GraphQL module using the gateway driver. We then provide a list of downstream services for resolution. Please note that in production configurations, it's advisable to use a service discovery.
import { IntrospectAndCompose } from '@apollo/gateway';
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloGatewayDriverConfig>({
      driver: ApolloGatewayDriver,
      server: {},
      gateway: {
        supergraphSdl: new IntrospectAndCompose({
          subgraphs: [ 
            { name: "authors", url: "http://localhost:3001/graphql" },
            { name: "books", url: "http://localhost:3002/graphql" },
          ],
        }),
      },
    }),
  ],
  providers: [],
})
export class AppModule {}
Let's start the author graph, book graph, and the supergraph. Query authors and books from the supergraph. You should be able to observe the same result as using the subgraph.

Resolving Relations

One of our primary objectives is to query all required fields with a single query. However, the book graph only has 'authorId,' and the author graph has no knowledge of books. In a REST API, we would need to fetch a book and then fetch the author by 'authorId.' The Supergraph resolves this issue by aggregating data from all subgraphs for us.
query book {
  book(id: 1) {
    id
    title
    author {
      id
      firstName
      lastName
    }
  }
}
query author {
  author(id: 1) {
    id
    firstName
    lastName
    books {
      id
      title
    }
  }
}
Our book schema contains 'authorId,' so we extend the 'author' from within the book subgraph. Interesting, isn't it?

Extend the author from the book

When we create the author schema with additional fields and an identifier, the Supergraph knows how to extend the schemas. Subgraphs don't contain any dependencies on other subgraphs.
import { Directive, Field, ID, ObjectType } from "@nestjs/graphql";
import { Book } from "./book.entity";

@ObjectType()
@Directive('@key(fields: "id")')
export class Author {

  @Field(() => ID!)
  id: number;

  @Field(() => [Book])
  books: [Book];
}
import { ObjectType, Field, Int, Directive, ID } from '@nestjs/graphql';
import { Author } from './author.entity';

@ObjectType()
@Directive('@key(fields: "id")')
export class Book {

  @Field(() => ID!)
  id: number;

  @Field(() => String)
  title: string;

  @Field(() => Int)
  authorId: number;

  @Field(() => Author, { nullable: true })
  author?: Author
}

Resolve author for a book

As our book schema now contains an 'author' field in addition to the 'authorId,' our book resolver should include a method to resolve the author. However, the book Microservice is unaware of the authors. What should we do in this case? We need to provide the necessary instructions for the Supergraph to resolve the author for books.
Here, we instruct the Supergraph by saying, 'Provide me this schema with this ID.' This is why we've decorated 'Id' with the '@key' decorator and defined a resolver method.
  @ResolveField(() => Author)
  author(@Parent() book: Book): any {
    return { __typename: 'Author', id: book.authorId };
  }

Resolve books for the author

When defining a schema, we need to define a resolver. Since the author schema is already resolved from the author graph, what we need to resolve are the additional properties.
import { Resolver, ResolveField, Parent } from '@nestjs/graphql';
import { BooksService } from './books.service';
import { Book } from './book.entity';
import { Author } from './author.entity';

@Resolver(() => Author)
export class AuthorResolver {

  constructor(private readonly booksService: BooksService) {}

  @ResolveField(() => Author)
  books(@Parent() author: Author): Book[] {
    return this.booksService.findByAuthorId(author.id);
  }
}
import { Module } from '@nestjs/common';
import { BooksService } from './books.service';
import { BooksResolver } from './books.resolver';
import { AuthorResolver } from './author.resolver';

@Module({
  providers: [BooksResolver, BooksService, AuthorResolver],
})
export class BooksModule {}
Restart all microservices and query books with authors, authors with books. Beautiful, isn't it?

Conclusion

GraphQL federation addresses many of the issues with REST and eliminates the need to create multiple BFFs by providing a unified federated API gateway.

Happy coding guys ☺

References

Apollo GraphQL Federation

Comments