Merge GraphQL Schemas Using Apollo Server and Koa
The article provides a step-by-step guide on how to merge schemas using these tools, making it easier for developers to organize and scale their GraphQL APIs.
Join the DZone community and get the full member experience.
Join For FreeToday, in our modern developer world, it is absolutely impossible to imagine life without such technologies as React, Node JS, GraphQL, and so on. They have solid ranks and are holding leading positions in data delivery. 70% of the cases I come across are projects that are integrated with GraphQL or are about to migrate to it. More and more companies prefer to use the GraphQL data query syntax, and today it is a piece of must-have knowledge.
GraphQL is a query-typed language for API which is widely used for requesting data from the server side to the client side in optimized mater. Clients request exactly what they need using typed schema. It allows you to send only what was requested instead of a fixed dataset.
Apollo Server gives you tools for sending responses to client requests. Apollo Client gives the ability to use GraphQL API, including cache and linking.
What Is It about?
We gonna create two Apollo Servers, which going to handle the GraphQL schema merge. It’s a situation when some external server responds to GraphQL API and some other service uses its own GraphQL schema, including external schema. On the Node layer, we going to wrap results up from the external server in one schema and send it to the client. Literally, we gonna just merge two schemas into one and send it to the client.
Let’s Dive Into the Code
For the implementation, we going to use NodeJS environment, Koa middleware, and Apollo Server with GraphQL Tools.
We have to run two servers. Both have to have a GraphQL Apollo Server. Here is the diagram.
Time to create boilerplates and run them both. For that, we need to create two folders and name one folder something like this: boilerplate-raphql-koa-server-external
and the second folder just like this: boilerplate-graphql-koa-server
Before starting, please take a look at the folder structure in both projects. Pretty straightforward. The difference between those two repos is going to be in the code.
├── package.json
└── src
├── index.js
├── resolvers.js
└── schema.js
External GraphQL Server
Now, let’s set up the boilerplate-graphql-koa-server-external
{
"name": "boilerplate-graphql-koa-server-external",
"version": "1.0.0",
"description": "Boilerplate GraphQL Koa server external",
"main": "src/index.js",
"scripts": {
"start": "PORT=4000 node src/index.js"
},
"engines": {
"node": "16.17.x"
},
"dependencies": {
"@graphql-tools/schema": "^9.0.2",
"@koa/cors": "^3.4.1",
"apollo-server-core": "^3.10.2",
"apollo-server-koa": "^3.10.2",
"graphql": "^15.8.0",
"koa": "^2.13.4",
"koa-graphql": "^0.12.0"
}
}
Then let’s create the server itself. In the src
folder in theindex.js
add server setup:
const Koa = require('koa');
const http = require('http');
const cors = require('@koa/cors');
const { ApolloServer } = require('apollo-server-koa');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');
async function server({ typeDefs, resolvers }) {
const app = new Koa();
const httpServer = http.createServer();
const apolloServer = new ApolloServer({
introspection: true,
schema: makeExecutableSchema({
typeDefs,
resolvers,
}),
});
await apolloServer.start();
apolloServer.applyMiddleware({ app, path: '/api/v1/graphql' });
httpServer.on('request', app.callback());
await new Promise(resolve => httpServer.listen({ port: process.env.PORT }, resolve));
console.log(
`External Server ready at http://localhost:${process.env.PORT}${apolloServer.graphqlPath}`
);
return { apolloServer, app };
}
server({ typeDefs, resolvers }).then(({ app }) => {
app.use(cors());
});
The async function server
will take care of the Koa app itself, and we are going to create the Apollo server with an executable schema where we have to provide types from schema and resolvers. From the official docs, we must call apopServer.start()
in advance before apolloServer.applyMiddleware
. It allows for identifying potential issues and taking action in the case of crushing the process in Apollo Server startup instead to start serving requests.
The second part is the boilerplate-graphql-koa-server-external
let's set up schema and resolvers.
const { gql } = require('apollo-server-koa');
module.exports = gql`
type Query {
getItemsExternal: [DataExternalExample]
}
type DataExternalExample {
id: ID
label: String
}
type Mutation {
updateDataExternal(label: String!): DataExternalExample!
}
`;
Resolvers for the schema.
const fakeData = {
id: 223421,
label: 'Some Label From External',
};
module.exports = {
Query: {
getItemsExternal: () => [fakeData],
},
Mutation: {
updateDataExternal: (_, { label }) => {
return {
...fakeData,
label,
};
},
},
};
Now it’s time to check the server responses. Before that, don’t forget to install the following packages: npm i
and then run the command npm run start
and put in the Chrome browser the URL: http://localhost:4000/api/v1/graphql
. Click on the button “Query your server,” and you can get the interface of Apollo GraphQL. It allows you to see the requested schema from the server. Open the Introspection Schema page. You will see there our schema:
If you were able to introspect the schema, then that means we are done with our boilerplate-graphql-koa-server-external
GraphQL Server for Merging Schemas
Let’s move now to boilerplate-graphql-koa-server
setups. Almost everything is the same in package.json
from external
but with additional packages and different PORT
, name, and description.
{
"name": "boilerplate-graphql-koa-server",
"description": "Boilerplate GraphQL Koa server",
"scripts": {
"start": "PORT=3000 node src/index.js"
},
"dependencies": {
"@graphql-tools/load": "^7.7.5",
"@graphql-tools/url-loader": "^7.14.1",
}
}
Let’s setup right away the new schema. There is pretty much the same but a bit different data in the schema.
const { gql } = require('apollo-server-koa');
module.exports = gql`
type Query {
getFakeDataExample: DataExample
}
type DataExample {
id: ID
value: String
}
type Mutation {
updateFakeData(value: String!): DataExample!
}
`;
And resolvers:
const fakeData = {
id: 4838745,
value: 'Some Random String',
};
module.exports = {
Query: {
getFakeDataExample: () => fakeData,
},
Mutation: {
updateFakeData: (_, { value }) => {
return {
...fakeData,
value,
};
},
},
};
And now, let’s take a look at the server file. You can find out that it’s relatively the same except few lines of code. First of all, we took the loadSchema
in order to get the external schema by request from EXTERNAL_ENDPOINT
which is our first launched server and the loader for the schema UrlLoader
.
The most important that we have to be sure that our schema has been loaded and the external server doesn’t throw any errors. We have to catch that situation. As you can see in the code, we got just an array of schemas. By default, we have only our own internalSchema
and then, if an external server is available, we are pushing to that array externalSchema
and then use the tool mergeSchemas
which helps to provide merged schema right to the ApolloServer
const Koa = require('koa');
const http = require('http');
const cors = require('@koa/cors');
const { ApolloServer } = require('apollo-server-koa');
const { loadSchema } = require('@graphql-tools/load');
const { UrlLoader } = require('@graphql-tools/url-loader');
const { makeExecutableSchema, mergeSchemas } = require('@graphql-tools/schema');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');
const EXTERNAL_ENDPOINT = 'http://localhost:4000/api/v1/graphql';
async function server({ typeDefs, resolvers }) {
const app = new Koa();
const httpServer = http.createServer();
const internalSchema = makeExecutableSchema({
typeDefs,
resolvers,
});
const schemas = [internalSchema];
try {
const externalSchema = await loadSchema(EXTERNAL_ENDPOINT, {
loaders: [new UrlLoader()],
});
schemas.push(externalSchema);
} catch {
console.warn('⚠️️ External Schema has not been loaded');
}
const apolloServer = new ApolloServer({
introspection: true,
schema: mergeSchemas({
schemas,
}),
});
await apolloServer.start();
apolloServer.applyMiddleware({ app, path: '/api/v1/graphql' });
httpServer.on('request', app.callback());
await new Promise(resolve => httpServer.listen({ port: process.env.PORT }, resolve));
console.log(`Server ready at http://localhost:${process.env.PORT}${apolloServer.graphqlPath}`);
return { apolloServer, app };
}
server({ typeDefs, resolvers }).then(({ app }) => {
app.use(cors());
});
Install all packages and run the server, which will be available on the PORT=3000
. Let’s go to the same interface of Apollo GraphQL, but the URL has to be with the proper PORT: http://localhost:3000/api/v1/graphql
. Now if we open the Introspection Schema page, we gonna able to see merged schemas. One from external and another one from the last created server.
Keep in mind that if some of your servers will get the same Field, the GraphQL server will rise the error something like this:
Error: Unable to merge GraphQL type “Query”: Field “getFakeDataExample”
already defined with a different type. Declared as “DataExample”,
but you tried to override with “DataExternalExample”
It means that we have to be very careful in a GraphQL schema with our Fields and Type definitions in order to not get into an awkward situation when the Type or Field already exists.
Conclusion
Numerous organizations are adopting a microservice architecture and attempting to isolate the data logic flow. The approach outlined above is particularly useful in situations where microservices communicate with each other within a company. Specifically, when there is a primary global service with a default schema and a secondary microservice with extra fields that may be used by the client in the future, this method allows developers to manage and scale their microservices more efficiently, thereby increasing the overall performance and agility of the system.
GitHub Repos
https://github.com/antonkalik/boilerplate-graphql-koa-server
https://github.com/antonkalik/boilerplate-graphql-koa-server-external
Published at DZone with permission of Anton Kalik. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments