Microservices with Graphql Gateway
One of the many reasons engineers love GraphQL is it's ability to query multiple data sources in one request. As engineering teams
move away from the traditional "REST" architecture, opportunities for new GraphQL Native
applications become possible. As you start
building out a distributed system with GraphQL the need for a short develop to deploy cycle becomes crucial. In a microservice architecture
with GraphQL, each service owns a GraphQL schema, a slice of the overall system's APIs. The graphql-gateway
service uses the concept of schema-delegation to combine multiple GraphQL schemas from different GraphQL servers to create a GraphQL Gateway.
GraphQL Gateways expose a single entry point to query your entire system.
Just because our API gateway has a GraphQL endpoint defined doesn't mean we can't have other endpoints too! It is entirely possible to define traditional REST routes in the same application.
Since we want to avoid duplicating code in the gateway, especially the code that makes requests out to the microservice, we have to choose which of our methodologies will be in charge. We could either dissect a GraphQL query and translate it into its corresponding REST requests, or we could translate a REST request into its GraphQL equivalent.
It turns out that the latter is simpler, so that's the trick we are proposing: translate requests made to REST routes into GraphQL. No code that interacts with the microservices needs to be duplicated because the REST routes simply act as a translation layer for GraphQL.
What do GraphQL gateways need to do?
A GraphQL API Gateway needs to handle:
- Lexing of the query Parsing
- Normalizations (removing whitespace, duplicate fields, etc.) Validation
- Enforcing field level authorization
- Calculating the complexity of the query
- Enforcing rate limits and quotas
- Printing the query (because we modified and cleaned it)
- Sending the request to the upstream
- Validating that the response conforms to the GraphQL schema
- Returning the response to the client
const { ApolloServer } = require('apollo-server');
const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway');
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [{ name: 'accounts', url: 'http://localhost:4001/graphql' }],
}),
});
const server = new ApolloServer({ gateway });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
Lets build the graphql Gateway for different graphl services
Building Graphql apollo federation Gateway using nestjs framework 🚀 🚀
You can check Part-1 of this Blog from here https://tkssharma.com/nestjs-with-apollo-federation-for-microservices-part-1
Everything we are talking here is in context of nestjs, You can write samethig using express or any other node js framework We will use nestjs to build services which will expose graphql interface
Lets build apollo federation Gateway
Guys, i have faced many challenged to build this example and repository and we still have a lot of mess created with nestjs/graphql versions with rest of the apollo ecosystems.
federation
These days we are only talking about microservice and with that how can we build distributed arcthitecture. Now a days GraphQL is becoming the preferred query language due to its flexibility. As we know microservices are difficult to work with. For example, how do you avoid multiple endpoints for users? One solution is to implement federation.
Before Federation cam einto picture we were doing those things with Schema stitching, we can just quicky check how both of these are different
Federation vs schema stitching
Schema stitching was the previous solution for microservice architecture. Both federation and schema stitching do offer the same functionality on the surface, gathering multiple services into one unified gateway, but the implementation is different.
With GraphQL federation, you tell the gateway where it needs to look for the different objects and what URLs they live at. The subgraphs provide metadata that the gateway uses to automatically stitch everything together. This is a low-maintenance approach that gives your team a lot of flexibility.
With schema stitching, you must define the “stitching” in the gateway yourself. Your team now has a separate service that needs to be altered, which limits flexibility. The use case for schema stitching is when your underlying services are not all GraphQL. Schema stitching allows you to create a gateway connected to a REST API, for example, while federation only works with GraphQL.
So when should you use either one? Many will say that federation is the overall winner, as it allows teams to focus on their application without needing to maintain a gateway. But if you have different types of APIs, you have to go with schema stitching.
In our example we are going tot alk about Graphql Federation. we can craete a simple nestjs application using nest-cli To get started, you can either scaffold the project with the Nest CLI, or clone a starter project (both will produce the same outcome).
To scaffold the project with the Nest CLI, run the following commands. This will create a new project directory, and populate the directory with the initial core Nest files and supporting modules, creating a conventional base structure for your project. Creating a new project with the Nest CLI is recommended for first-time users. We'll continue with this approach in First Steps.
$ npm i -g @nestjs/cli
$ nest new project-name
Alternatives# Alternatively, to install the TypeScript starter project with Git:
$ git clone https://github.com/nestjs/typescript-starter.git project
$ cd project
$ npm install
$ npm run start
Lets start by adding required dependancies
"dependencies": {
"@apollo/gateway": "0.46.0",
"@nestjs/apollo": "9.2.4",
"@nestjs/common": "8.2.3",
"@nestjs/core": "8.2.3",
"@nestjs/graphql": "10.0.0",
"@nestjs/platform-express": "8.2.3",
"apollo-server-express": "3.6.2",
"dotenv": "^16.0.0",
"graphql": "15.7.2",
"graphql-tools": "8.0.0",
"graphql-upload": "^13.0.0",
"jsonwebtoken": "^8.5.1",
"reflect-metadata": "0.1.13",
"rimraf": "3.0.2",
"rxjs": "7.4.0",
"ts-morph": "12.2.0"
}
With the latest Migration https://docs.nestjs.com/graphql/migration-guide its now easy to build a simple apollo federation gateway without using any other external librray
import { RemoteGraphQLDataSource } from '@apollo/gateway';
import {
Module,
BadRequestException,
HttpStatus,
HttpException,
UnauthorizedException,
MiddlewareConsumer,
} from '@nestjs/common';
import { IntrospectAndCompose } from '@apollo/gateway';
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo';
import { GraphQLModule } from '@nestjs/graphql';
GraphQLModule.forRoot <
ApolloGatewayDriverConfig >
{
driver: ApolloGatewayDriver,
gateway: {
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'User', url: 'http://localhost:5006/graphql' },
{ name: 'Home', url: 'http://localhost:5003/graphql' },
{ name: 'Booking', url: 'http://localhost:5004/graphql' },
],
}),
},
};
With recent nestjs/graphql 10.x Migration we can just pass driver and same GraphqlModule will work as gateway Module
GraphQLModule.forRoot <
ApolloGatewayDriverConfig >
{
driver: ApolloGatewayDriver,
};
And Gateway defination contains list of all sub-graph services
gateway: {
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'User', url: 'http://localhost:5006/graphql' },
{ name: 'Home', url: 'http://localhost:5003/graphql' },
{ name: 'Booking', url: 'http://localhost:5004/graphql' },
],
}),
}
Lets hava e look into the whole defination of file
import { RemoteGraphQLDataSource } from '@apollo/gateway';
import {
Module,
BadRequestException,
HttpStatus,
HttpException,
UnauthorizedException,
MiddlewareConsumer,
} from '@nestjs/common';
import { IntrospectAndCompose } from '@apollo/gateway';
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo';
import { GraphQLModule } from '@nestjs/graphql';
import { verify, decode } from 'jsonwebtoken';
import { INVALID_AUTH_TOKEN, INVALID_BEARER_TOKEN } from './app.constants';
import { graphqlUploadExpress } from 'graphql-upload';
const getToken = (authToken: string): string => {
console.log(authToken);
const match = authToken.match(/^Bearer (.*)$/);
if (!match || match.length < 2) {
throw new HttpException({ message: INVALID_BEARER_TOKEN }, HttpStatus.UNAUTHORIZED);
}
console.log(match[1]);
return match[1];
};
const decodeToken = (tokenString: string) => {
const decoded = verify(tokenString, process.env.SECRET_KEY);
if (!decoded) {
throw new HttpException({ message: INVALID_AUTH_TOKEN }, HttpStatus.UNAUTHORIZED);
}
return decoded;
};
const handleAuth = ({ req }) => {
try {
if (req.headers.authorization) {
const token = getToken(req.headers.authorization);
const decoded: any = decodeToken(token);
return {
userId: decoded.userId,
permissions: decoded.permissions,
authorization: `${req.headers.authorization}`,
};
}
} catch (err) {
throw new UnauthorizedException('User unauthorized with invalid authorization Headers');
}
};
@Module({
imports: [
GraphQLModule.forRoot <
ApolloGatewayDriverConfig >
{
server: {
context: handleAuth,
},
driver: ApolloGatewayDriver,
gateway: {
buildService: ({ name, url }) => {
return new RemoteGraphQLDataSource({
url,
willSendRequest({ request, context }: any) {
request.http.headers.set('userId', context.userId);
// for now pass authorization also
request.http.headers.set('authorization', context.authorization);
request.http.headers.set('permissions', context.permissions);
},
});
},
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'User', url: 'http://localhost:5006/graphql' },
{ name: 'Home', url: 'http://localhost:5003/graphql' },
{ name: 'Booking', url: 'http://localhost:5004/graphql' },
],
}),
},
},
],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(graphqlUploadExpress()).forRoutes('graphql');
}
}
This contains a lots of code, lets talk about all these one by one
- this gateway is composing all subgraphs
- this gateway is managing authorization also by decoding token
- this gateway is building a request resource by
RemoteGraphQLDataSource
before making actual api call to downstream service - this gateway is sending downstream request to list of a particulat subgraphs based on what client in requesting from query
GraphQLModule.forRoot <
ApolloGatewayDriverConfig >
{
server: {
context: handleAuth,
},
};
const handleAuth = ({ req }) => {
try {
if (req.headers.authorization) {
const token = getToken(req.headers.authorization);
const decoded: any = decodeToken(token);
return {
userId: decoded.userId,
permissions: decoded.permissions,
authorization: `${req.headers.authorization}`,
};
}
} catch (err) {
throw new UnauthorizedException('User unauthorized with invalid authorization Headers');
}
};
context handleAuth
will validate authorization header and return data in context so we can access context payload further
buildService: ({ name, url }) => {
return new RemoteGraphQLDataSource({
url,
willSendRequest({ request, context }: any) {
request.http.headers.set('userId', context.userId);
// for now pass authorization also
request.http.headers.set('authorization', context.authorization);
request.http.headers.set('permissions', context.permissions);
},
});
};
buildService
is building a request for all sub-graphs by sending data in headers from context, in this example we are sending user metadata in headers for the request.
So this is all about our gateway service which can talk and compose all sub-graphs into one single api endpoint. After starting application
[Nest] 6612 - 05/23/2022, 11:25:39 AM LOG [NestFactory] Starting Nest application...
[Nest] 6612 - 05/23/2022, 11:25:39 AM LOG [InstanceLoader] AppModule dependencies initialized +30ms
[Nest] 6612 - 05/23/2022, 11:25:39 AM LOG [InstanceLoader] GraphQLSchemaBuilderModule dependencies initialized +1ms
[Nest] 6612 - 05/23/2022, 11:25:39 AM LOG [InstanceLoader] GraphQLModule dependencies initialized +0ms
[Nest] 6612 - 05/23/2022, 11:25:39 AM LOG [NestApplication] Nest application successfully started +190ms
Our gateay is ready but we must also need all these sub-graphs ready otherwise it will throw error, so our first task is to get all these sb-graphs running on all those ports
{ name: 'User', url: 'http://localhost:5006/graphql' },
{ name: 'Home', url: 'http://localhost:5003/graphql' },
{ name: 'Booking', url: 'http://localhost:5004/graphql' },
Now lets build simple graphql services so we can see how we can share data across different microservices and how this gateway is composing data together
Conclusion
This was just a quick introduction on what is apollo graphql federation gateway, lets explore more about this in our next Blog where we will check actual code implementation of graphql Gateway in nestjs. Lets take a look on Part-3 https://tkssharma.com/nestjs-with-apollo-federation-for-microservices-part-3
References
- https://www.apollographql.com/docs/federation/
- https://www.apollographql.com/docs/federation/federation-2/new-in-federation-2
- https://github.com/apollographql/supergraph-demo-fed2
- https://www.apollographql.com/docs/federation/federation-spec/
- https://docs.nestjs.com/graphql/migration-guide
- https://docs.nestjs.com/graphql/federation
Comments