Nest JS Graphql with Apollo Federation Gateway for Microservices #part-4

tkssharma - May 29 '22 - - Dev Community

Building Graphql Service (Home APIs) 🚀 🚀

You can check Part-1/2/3 of this Blog from here, In this example we will build another graphql service Home APIs

Github :
https://github.com/tkssharma/nestjs-with-apollo-federation-gateway

To understand the whole architecture we need to have

  • gateway Nestjs service (Gateway-service)
  • graphql User service (Microservice-1)
  • graphql Home manager service (Microservice-2)

Overall we have one gateway and two microservice exposing graphql interface.

In this Post we are building Home Service and Most importantly we will see how gateway is composing all sub-graphs and fetching data together for us from different graphs

  • The whole Objective of this series is how we can fetch Home APIs data with User Data, How gateway is composing both data together where data is here in different service databases

Lets Build Home APIs

Its lerna project and we have these 3 modules

  • gateway nestjs module
  • user auth api module
  • home service api module

All graphql services using schema first approach that means first we will write schema and then we will write

  • resolvers
  • services
  • modules
  • root modules

Resolvers will map to the query and mutations we have and interact with services to fetch data
Lets check folder structure

drawing
  • We are building simple home crud APIs, Lets create basic things

  • Home Graphql Schema

  • Home Module

  • Home Service

  • Home Entity

  • Home DTO

  • Home Resolver

Home Entity

import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne, CreateDateColumn, DeleteDateColumn, UpdateDateColumn, OneToOne, JoinColumn, OneToMany } from 'typeorm';
@Entity('homes')
export class Home extends BaseEntity {
  @PrimaryGeneratedColumn('uuid')
  public id!: string;

  @Column('varchar', { length: 500, unique: true })
  public name!: string;

  @Column({ type: 'uuid' })
  public user_id!: string;


  @Column('varchar')
  public description!: string;

  @Column({ type: 'jsonb', default: [] })
  public display_images!: string[];

  @Column({ type: 'jsonb', default: [] })
  public original_images!: string[];

  @Column({ type: 'jsonb', default: null })
  public metadata!: any;

  @Column({ type: 'boolean', default: true, select: true })
  public is_active!: boolean;

  @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
  public created_at!: Date;

  @UpdateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
  public updated_at!: Date;

  @DeleteDateColumn()
  public deleted_at?: Date | null;

}
Enter fullscreen mode Exit fullscreen mode

Home Graphql Schema

scalar Date
scalar Upload

type Home @key (fields: "id"){
    id: ID!
    name: String!
    user: User
    description: String!
    display_images: [String!]
    original_images: [String!]
    is_active: Boolean!
    created_at: Date
    updated_at: Date
}

extend type User @key(fields: "id") {
  id: ID! @external
}

type Query {
    homes: [Home!]
    home(id: ID): Home!
    findHomes(name: String!): [Home!]
    activeHomes: [Home!]
}
input HomeInput {
  name: String!
  description: String!
  is_active: Boolean!
}

type Mutation {
    createHome(payload: HomeInput!): Home
    updateHome(id: ID!, payload: HomeInput!): Home
}
Enter fullscreen mode Exit fullscreen mode

Lets take a close look here We have home Schema and we are extending User Type here from other service

type Home @key (fields: "id"){
    id: ID!
    name: String!
    user: User
    description: String!
    display_images: [String!]
    original_images: [String!]
    is_active: Boolean!
    created_at: Date
    updated_at: Date
}

extend type User @key(fields: "id") {
  id: ID! @external
}
Enter fullscreen mode Exit fullscreen mode

Lets add resolver which should be simple data fetch from services

import { AdminGuard } from '@app/auth/guards/admin.guard';
import { JwtAuthGuard } from '@app/auth/guards/jwt-auth.guard';
import { Logger } from '@logger/logger';
import { ConsoleLogger, UseGuards } from '@nestjs/common';
import { Resolver, Query, Args, Mutation, Parent, ResolveField, Context, ResolveReference } from '@nestjs/graphql';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { HomeLocality } from '../entity/home-locality.entity';
import { Home } from '../entity/home.entity';
import { HomeService } from './home.service';

@Resolver((of: any) => Home)
export class HomeResolver {
  constructor(private homeService: HomeService,
    private readonly logger: Logger) {
  }

  @Query()
  @UseGuards(JwtAuthGuard, AdminGuard)
  async homes() {
    return await this.homeService.listAll();
  }

  @Query()
  @UseGuards(JwtAuthGuard, AdminGuard)
  async findHomes(@Args('name') name: string) {
    return await this.homeService.findHome(name);
  }

  @Query()
  @UseGuards(JwtAuthGuard, AdminGuard)
  async activeHomes() {
    return await this.homeService.listAllActiveHomes();
  }

  @Query()
  async home(@Args('id') id: string) {
    return await this.homeService.getById(id);
  }

  @Mutation()
  @UseGuards(JwtAuthGuard, AdminGuard)
  async createHome(@Args() args: any, @Context() context: any) {
    const { userid } = context.req.headers;
    return await this.homeService.createHome(args, userid);
  }

  @Mutation()
  @UseGuards(JwtAuthGuard, AdminGuard)
  async updateHome(@Args('id') id: string, @Args() args: any) {
    return await this.homeService.updateHome(id, args);
  }
  @ResolveField('user')
  user(@Parent() home: Home) {
    this.logger.http("ResolveField::user::HomeResolver" + home.user_id)
    return { __typename: 'User', id: home.user_id };
  }

  @ResolveReference()
  async resolveReference(reference: { __typename: string; id: string }) {
    this.logger.http('Logging :: ResolveReference :: home')
    return await this.homeService.getByHomeId(reference.id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Everything is simple here except two methods which you see in the last of the resolvers
and they are the one responsible for fetching data cross services
ResolveField, ResolveReference
ResolveReference - means when any other services need Home data based on ID this method graphql gateway will invoke
ResolveField - this is reference so when in home graphql if i want to get data of User then i have to resolve that data from another service @ResolveField('user') this will ask gateway to resolve User by passing { __typename: 'User', id: home.user_id } this to the gateway, Now gateway will check who can give me this User Type for this id as userId

Notes -- Very very Important [keep TypeORM entity class name same as Graphql Types such as User and Home]

  @ResolveField('user')
  user(@Parent() home: Home) {
    this.logger.http("ResolveField::user::HomeResolver" + home.user_id)
    return { __typename: 'User', id: home.user_id };
  }

  @ResolveReference()
  async resolveReference(reference: { __typename: string; id: string }) {
    this.logger.http('Logging :: ResolveReference :: home')
    return await this.homeService.getByHomeId(reference.id);
  }
Enter fullscreen mode Exit fullscreen mode

You can also access the context if its has ben set already

  async createHome(@Args() args: any, @Context() context: any) {
    const { userid } = context.req.headers;
    return await this.homeService.createHome(args, userid);
  }
Enter fullscreen mode Exit fullscreen mode

Now service is just using TypeORM Repo to fetch data for Us

import { Logger } from '@logger/logger';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ILike, Like, Repository } from 'typeorm';
import { HomeFacility } from '../entity/home-facility.entity';
import { HomeLocality } from '../entity/home-locality.entity';
import { Home } from '../entity/home.entity';
import { CreateHomeDto } from './home.dto';


@Injectable()
export class HomeService {
  constructor(
    @InjectRepository(Home) private readonly homeRepository: Repository<Home>,
    @InjectRepository(HomeLocality) private readonly homeLocalityRepository: Repository<HomeLocality>,
    private readonly logger: Logger
  ) {
  }

  async createHome(data: any, userid: string): Promise<Home> {
    const body = data.payload;
    try {
      const existingHome = await this.homeRepository.findOne({ where: { name: body.name } })
      if (existingHome) {
        return existingHome;
      }
      const res = await this.homeRepository
        .save({ ...body, user_id: userid });
      return res;
    } catch (err: any) {
      this.logger.error(err);
      throw err;
    }
  }


  async updateHome(id: string, data: any): Promise<Home> {
    const body = data.payload;
    const homeHome = await this.homeRepository.findOne({ where: { id } });
    const updatedHome = { ...homeHome, ...body }
    return await this.homeRepository.save(updatedHome)
  }

  async listAll() {
    return await this.homeRepository.find({ relations: ['locality', 'facilities'] });
  }

  async findHome(name: string) {
    return await this.homeRepository.find({ where: { name: ILike(`%${name}%`) }, relations: ['locality', 'facilities'] });
  }

  async listAllActiveHomes() {
    return await this.homeRepository.find({ where: { is_active: true }, relations: ['locality', 'facilities'] });
  }

  async getById(id: string) {
    return await this.homeRepository.findOne({ where: { id, is_active: true }, relations: ['locality', 'facilities'] });
  }

  async getByHomeName(name: string) {
    return await this.homeRepository.findOne({ where: { name } });
  }

  async getByHomeId(id: string) {
    return await this.homeRepository.findOne({ where: { id } });
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally our Domain Module

import { MiddlewareConsumer, Module } from '@nestjs/common';
import { TypeOrmModule, TypeOrmModuleAsyncOptions } from '@nestjs/typeorm';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { ConfigModule } from '@app/config/config.module';
import { DbModule } from '../../db/db.module';
import { Home } from './entity/home.entity';
import { HomeModule } from './home/home.module';
import { LoggerModule } from '@logger/logger.module';
import { Upload } from '../Scalars/upload.scalar';

import { ApolloFederationDriver, ApolloFederationDriverConfig } from '@nestjs/apollo';
import { GraphQLError, GraphQLFormattedError } from 'graphql';

@Module({
  imports: [
    DbModule.forRoot({
      entities: [Home],
    }),
    LoggerModule,
    HomeModule,
    ConfigModule,
    GraphQLModule.forRoot({
      typePaths: ['./**/*.graphql'],
      uploads: false,
      driver: ApolloFederationDriver,
      context: ({ req }: any) => ({ req }),
      formatError: (error: GraphQLError) => {
        console.log(error);
        const graphQLFormattedError: GraphQLFormattedError = {
          message: error?.extensions?.exception?.response?.message || error?.message,
        };
        return graphQLFormattedError;
      },
    }),
  ],
})
export class DomainModule {}
Enter fullscreen mode Exit fullscreen mode

This module also using ApolloFederationDriver so these services can use schema from other services
From the whole code the real magic is happening in schema

extend type User @key(fields: "id") {
  id: ID! @external
}
Enter fullscreen mode Exit fullscreen mode

All these @key, @provided , @external is provided in apollo specs

@key

directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
Enter fullscreen mode Exit fullscreen mode

The @key directive is used to indicate a combination of fields that can be used to uniquely identify and fetch an object or interface.

type Product @key(fields: "upc") {
  upc: UPC!
  name: String
}
Enter fullscreen mode Exit fullscreen mode

Multiple keys can be defined on a single object type:

@provides

directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
The @provides directive is used to annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. Given the following example:

type Review @key(fields: "id") {
  product: Product @provides(fields: "name")
}

extend type Product @key(fields: "upc") {
  upc: String @external
  name: String @external
}
Enter fullscreen mode Exit fullscreen mode

When fetching Review.product from the Reviews service, it is possible to request the name with the expectation that the Reviews service can provide it when going from review to product. Product.name is an external field on an external type which is why the local type extension of Product and annotation of name is required.

@requires

directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
The @requires directive is used to annotate the required input fieldset from a base type for a resolver. It is used to develop a query plan where the required fields may not be needed by the client, but the service may need additional information from other services. For example:

# extended from the Users service
extend type User @key(fields: "id") {
  id: ID! @external
  email: String @external
  reviews: [Review] @requires(fields: "email")
}
Enter fullscreen mode Exit fullscreen mode

In this case, the Reviews service adds new capabilities to the User type by providing a list of reviews related to a user. In order to fetch these reviews, the Reviews service needs to know the email of the User from the Users service in order to look up the reviews. This means the reviews field / resolver requires the email field from the base User type.

@external

directive @external on FIELD_DEFINITION
The @external directive is used to mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. For example:

# extended from the Users service
extend type User @key(fields: "email") {
  email: String @external
  reviews: [Review]
}
Enter fullscreen mode Exit fullscreen mode

lets See this in action

  • we need docker-compose up so we can have all database available for TypeORM
  • docker-compose up will crate container
  • install package dependencies in all projects
  • run applications and final run gateway

Setting up the whole platform Locally

This platform contains all these components

  • User Management service
  • Home Manager service
  • Gateway Service
  • Home Manager Service

Running all these services

  • we are using docker-compose to bootstrap all container only (database containers) in the root of the project run
docker-compose up
Enter fullscreen mode Exit fullscreen mode
  • check the longs and make sure databases has been created
git clone <repo>
cd nestjs-with-apollo-federation-gateway
cd packages
Enter fullscreen mode Exit fullscreen mode

Running Auth service

cd auth-service
vi .env
Enter fullscreen mode Exit fullscreen mode

update env with this content

DATABASE_URL= postgres://api:development_pass@localhost:5431/auth-api
SENDGRID_API_KEY=SSSS
SENDGRID_VERIFIED_SENDER_EMAIL=ssss@gmail.com
DEBUG="ssss:*"
LOG_LEVEL=http
PORT=5006
NODE_ENV=local
JWT_SECRET=HELLO
JWT_EXPIRE_IN=3600*24
Enter fullscreen mode Exit fullscreen mode

Now run application in watch mode it will be live on localhost:5006

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

Running Home Manager

cd home-manager
vi .env
Enter fullscreen mode Exit fullscreen mode

update env with this content

NODE_ENV=local
LOG_LEVEL=http
PORT=5003
SECRET_KEY=HELLO
NEW_RELIC_KEY=
DATABASE_URL=postgres://api:development_pass@localhost:5433/home-manager-api
Enter fullscreen mode Exit fullscreen mode

Now run application in watch mode it will be live on localhost:5003

npm run start:dev
Enter fullscreen mode Exit fullscreen mode
  • docker-compose up
  • Auth Apis npm run start:dev
  • Home Apis npm run start:dev

For Gateway also trigger same command npm run start:dev

Now lets test our application
Our Gateway is connected to both sub-graphq apis running on different ports
We have to make sure both service are running before running gateway service

{ name: 'User', url: 'http://localhost:5006/graphql' },
{ name: 'Home', url: 'http://localhost:5003/graphql' },
Enter fullscreen mode Exit fullscreen mode

If you are using same PORT and env then Gateway will run on http://localhost:5002/graphql

graphql client

Conclusion

I hope this series is useful for devs who are looking for setting up Nest JS graphql gateway with federation support
Important Pats and challenging tasks
Notes ::

References

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .