Photo by https://www.apollographql.com/

Apollo Federation, TypeGraphQL, and Reference Resolvers

Enable GraphQL reference resolvers with Apollo Federation and TypeGraphQL Framework

Sunday, Sep 5, 2021

avatargraphql
avatartypescript

After spending several frustrating hours trying to get reference resolvers to work with Apollo Federation & TypegraphQL, decided to write up how to get it setup.

Full Github Project Here

Apollo Federation Recap

Apollo Federation gives us the ability to decouple monolithic services with large schemas and graphs into smaller subgraphs. Enabling to break up your architecture into individual microservices.

To communicate with all the subgraphs and schemas, a single gateway (known as the "Gateway") sits in front and acts as a single point of entry for frontend applications.

This architecture improves security by the gateway being the only microservice needing to be publicly accessible to the internet. Just have to make sure to configure the appropriate firewall rules and network access control lists to allow the Gateway to access all of the subgraphs.

More on Apollo Federation


TypegraphQL

What is it?

TypegraphQL is a modern Typescript framework where you can build GraphQL APIs in Node.js with classes and decorators

More on TypeGraphQL



Reference Resolvers and their use case

With having a federated graph, you might need to access related entities that are housed in another subgraph. Not to be confused with fetching unrelated entities from 1 API call and resolving after the request has been made.

Such as

query {
  posts { ... }
  users { ... }
}

Common example would be resolving "Users" from multiple subgraphs.

Imagine you have an individual subgraph microservice that you store your User table and information in.

Then in another microservice, say "Posts", you store the user id of the person who created the post, along with other post information.

First hunch when formulating a query strategy would be to grab posts and users, then resolve the information in the frontend to match the appropriate user information by its id.

Separate

query {
  posts {
    id
    createdById
  }
  users {
    id
    username
  }
}

However, with the help of Reference Resolvers, a query like this is possible instead.

Referenced

query {
  posts {
    id
    createdById
    createdByUser {
      id
      username
    }
  }
}


Resolving User subgraph data from the Post subgraph

We'll be enabling the graphql query above with the help of TypegraphQL and Apollo Federation.


First in the User subgraph, need to first update your TypeGraphQL decorated User entity.

Need to add the @Directive decorator passing in the @key directive, specifying the fields used as the primary key used to uniquely identify and fetch the User object. In this case, the unique identifier would be the id.

user/src/User/User.ts

import { ObjectType, Field, Directive } from 'type-graphql'
 
@Directive(`@key(fields: "id")`)
@ObjectType()
export class User {
 
    @Field()
    id: string;
 
    @Field()
    username: string;
 
    @Field()
    name: string;
 
    @Field()
    birthDate: string;
}

This should generate the following SDL in the User graph

type User @key(fields: "id") {
  id: String!
  username: String!
  name: String!
  birthDate: String!
}


Next, we'll have to create a reference resolver for the User entity in order for the gateway to know how to resolve the entire User object.

users/src/User/user-reference.ts

import type { GraphQLResolveInfo } from 'graphql';
import { User } from './User';
import { userData } from './data'; // mock data
 
export const resolveUserReference = async (
  reference: Pick<User, 'id'>,
  ctx: Record<string, any>,
  info: GraphQLResolveInfo
): Promise<User> => {
  // would replace with your ORM of choice
  const user = userData.find(({ id }) => reference.id === id);
  return user;
}

Here in the resolveUserReference function, we're telling apollo federation that we can resolve the entire User entity, we just expect as input a incomplete User type with just an id property. (Our other microservices can store this id value in their databases and supply this partial User object)



Last step in our User microservice, pass resolveUserReference, into our buildFederatedSchema function.

import 'reflect-metadata';
import { ApolloServer } from 'apollo-server';
 
import { buildFederatedSchema } from '@federation/common';
import { UserResolver } from './User/resolver';
import { resolveUserReference } from './User/user-reference'
 
(async () => {
 
    const schema = await buildFederatedSchema(
      {
        resolvers: [UserResolver],
      },
      {
        User: {
          __resolveReference: resolveUserReference
        }
      }
    );
 
    const server = new ApolloServer({ schema })
 
    const { url } = await server.listen({ port: 3001 });
 
    console.log(`user subgraph ready at ${url}`);
 
})();


Now that our User entity is ready to be resolved from another microservice, we need to update the example Post subgraph.

post/src/User/User.ts

import { ObjectType, Field, Directive } from 'type-graphql'
 
@Directive(`@extends`)
@Directive(`@key(fields: "id")`)
@ObjectType()
export class User {
 
    @Directive(`@external`)
    @Field()
    id: string;
}

Here we add a few decorators to the User Model in the post microservice. Just like in the User microservice, we have the @key directive along with an additional @extends directive.

This @extends directive lets the gateway know that we wish to extend the original subgraph where the User is from.

Finally, we have the @external Directive, decorated over the id Field. This indicates to the gateway that the field originates in another subgraph.



post/src/Post/Post.ts

import { ObjectType, Field, Root } from 'type-graphql'
import { User } from '../User/User';
 
@ObjectType()
export class Post {
 
    @Field()
    id: string;
 
    @Field()
    name: string;
 
    @Field()
    createdById: string;
 
    @Field(() => User)
    createdByUser(@Root() { createdById }: Post): User {
        return { id: createdById }
    }
}


Now in the Post entity file, notice we have a @Field decorator resolving a type of User, and we're returning an object with a singular property of id, set by the stored createdById field.

We use TypeGraphQL's @Root directive to be able to reference other fields on this object in the createdByUser function.

This createdByUser object will then get passed into the resolveUserReference function we created prior in the external User subgraph.



Now that has all been set up, we can resolve the entire User type from our Post query through the gateway server.

query

query {
  Post(id:"12345") {
    id
    createdById
    createdByUser {
      id
      name
      username
      birthDate
    }
  }
}

response

{
  "data": {
    "Post": {
      "id": "12345",
      "createdById": "1",
      "createdByUser": {
        "id": "1",
        "name": "John Smith",
        "username": "test123",
        "birthDate": "\"2021-09-04T19:00:01.709Z\""
      }
    }
  }
}

Now from a single fetch of a Post, we have the related createdByUser information from our User subgraph. No need to do any mapping client side after the graphql request has been resolved.



Resolving Users created posts from the Post subgraph

Now since we established that the User type is extended, external, with the @key directive specifying the unique identifies, we can do the opposite.

We can also grab all posts that users have created, to make this query possible

query {
  users {
    id
    username
    posts {
      id
      name
    }
  }
}

In the Post microservice, we'll add a new Resolver, that resolves to a User type.

post/src/User/resolver.ts

import { Resolver, FieldResolver, Root } from "type-graphql";
 
import { User } from './User';
import { Post } from '../Post/Post';
import { postData } from "../Post/data"; // mock data
 
@Resolver(() => User)
export default class UserPostsResolver {
 
    @FieldResolver(() => [Post])
    async posts(@Root() user: User): Promise<Post[]> {
      // would replace with your ORM of choice
      return postData.filter(post => post.createdById === user.id)
    }
}

In this UserPostsResolver, we have a @FieldResolver that returns an array of Posts, and generates the following SDL

type User @extends @key(fields: "id") {
  id: String! @external
  posts: [Post!]!
}

Now that apollo gateway server knows how to resolve fetching posts created by users, we can complete the query shown above.

query

query {
  user {
    id
    username
    posts {
      id
      name
    }
  }
}

response

{
  "data": {
    "user": {
      "id": "1",
      "username": "test123",
      "posts": [
        {
          "id": "12345",
          "name": "accounts"
        }
      ]
    }
  }
}

Final Thoughts

Even though it takes a lot of boilerplate code to get this working with TypeGraphQL, I'm still a fan of this library.

TypeGraphQL has an Apollo Federation example, have it linked below and highly recommend to go check out. In the example it wasn't exactly clear to me how to resolve data that wasn't in one microservice.

If you had trouble trying to get reference resolvers working as well hope this helps.

Happy Coding!



Additional Resources