GraphQL — how we improved our CMS platform, using a generic infrastructure!

Michael Kibenko
NI Tech Blog
Published in
6 min readJun 1, 2022

I remember the time when GraphQL become popular, at that time I worked as an Android and BackEnd team leader. Back then, we had a lot of thinking about using GraphQL, but in the end, we decided to use REST because GraphQL was really new for everybody and we had no time for any risky development. The second time I came across GraphQL was in the case of improving the CMS engine at Natural Intelligence, which is being used and modified across the company's R&D teams, this time it was a natural decision to go with GraphQL.
In this article, I want to share how we built the GraphQL infrastructure and how we are using GraphQL in production.

First, why we decided using GraphQL?

  • Over-fetching: we saw that by using REST in order to render a UI for our user we are fetching a lot of useless data, this made the requests to be slower, and made all the cluster networks heavier, we wanted to fetch just the needed data for the UI.
  • Schema: by using standard REST you don't really know what will be the result of the call, of course, you can look at the database schema or swagger, but in many cases, you have data mutations as a pre response hooks and don't have swagger. By writing GraphQL queries the developer must be aware of the schema and if something does not exist in the schema the developer will instantly receive a validation error.
import { gql } from '@apollo/client';
import { omitBy, isEmpty } from 'lodash';
export const query = gql`
query Sites($queryParams: JSONObject, $count: Int, $offset: Int) {
sites(queryParams: $queryParams, count: $count, offset: $offset){
id
siteId
createdAt
createdByUser
title
statistics {
isCurrentPublished
wasPublished
totalVariants
defaultPublishedVariant {
createdByUser
createdAt
publishAt
}
}
}
}
`;
export const getVariables = props => ({
queryParams: omitBy(props.query, isEmpty),
count: 50,
offset: 0,
});
export default query;
  • Parallelism: we saw in most cases parallel and queued requests originating from the client-side application, after the response, we aggregated the returned data to UI logical structure. We wanted to reduce the client-side decision-making and move all the old query logic to the server-side. We decided to take an out-of-the-box solution and use apollo gateway. By using apollo gateway we can now make just one server-side call and let the gateway decide what microservices (resolvers) to call in order to execute the query, make it parallel or queued, and aggregate the data using our initial query.
Apollo gateway Architecture
  • Schema safeness: we wanted our schemas to be safe, and always be backward compatible for fields underuse. In order to make it we added the apollo check mechanism in a PR level for every microservice and from now on if somebody removed or changed a used field, the apollo check process will fail and you will not be able to deploy your change (if your change safe the apollo studio admin can mark this change as safe and let the process move forward). After a change is marked as safe, as a part of the deployment process, we are executing the apollo publish process which uploads the new schema to apollo studio.
Apollo checks
  • GraphQL Playground: we wanted teams to easily play with our data without thinking about REST and easily see all the schemas in one place, in order to make it we integrated GraphQL playground as a part of our application, and made it available for our relevant internal users.
GraphQL Playground

So, how did we implement it?

We have many similar flows and rely on a stable controller, service, and repository architecture layers (can read more about it here). We decided to create a package to include common resolvers, definitions, and query builders which will be used in every microservice. This solution can be extended to support any specific use case, the idea is that every builder will have an injectable service layer and options parameters that will be used in order to resolve the data.

A sample generic resolvers:

import queryParamsType from './types/queryParams.interface';
import Service from './types/service.interface';
export interface ResolverDTO {
service: Service;
}
export interface ResolverWithParamDTO extends ResolverDTO {
paramName: string;
}
export const getEntities =
({ service }: ResolverDTO) =>
async (parent: object, args: queryParamsType) => {
const result = await service.getEntities(args);
return result.content;
};
export const getById =
({ service }: ResolverDTO) =>
async (parent: object, args: queryParamsType) => {
const { id } = args;
const result = await service.getEntity(id);
return result.content;
};
export const getExternalsById =
({ service, paramName }: ResolverWithParamDTO) =>
async (parent: object) => {
const parentIdParam = parent[paramName];
const result = await service.getEntity(parentIdParam);
return result.content;
};
export default {
getEntities,
getById,
getExternalsById,
};

Generic definition builders:

import niPluralize from '@naturalintelligence/pluralize';
import Service from './types/service.interface';
import genericResolvers from './genericResolvers';export interface DefinitionsDTO {
entityName: string;
service: Service;
}
export const getEntities = ({ entityName, service }: DefinitionsDTO): object => ({
[`${niPluralize(entityName)}`]: genericResolvers.getEntities({ service }),
});
export const getById = ({ entityName, service }: DefinitionsDTO): object => ({
[`${entityName}ById`]: genericResolvers.getById({ service }),
});
export const getDefault = ({ entityName, service }: DefinitionsDTO): object => ({
...getEntities({ entityName, service }),
...getById({ entityName, service }),
});
export default = getDefault;

Resolvers implementation within the microservice:

const {
backOffice: {
resolvers: { genericResolvers, definitions },
},
} = require('@naturalintelligence/shared-graphql');
const productsService = require('../services/products-service');
const sitesService = require('../services/sites-service');
const commonEntityResolvers = {
site: genericResolvers.getExternalsById({
service: sitesService,
paramName: 'siteId',
}),
};
const resolvers = {
Product: commonEntityResolvers,
Query: {
...definitions.getDefault({ entityName: 'product', service: productsService })
},
};
module.exports = {
resolvers,
};

Generic typedefs in the shared package:

import { upperFirst } from 'lodash';
import niPluralize from '@naturalintelligence/pluralize';
export interface QueryDTO {
entityName: string;
returnableType?: string;
}
export const getEntitiesQuery = ({ entityName, returnableType = upperFirst(entityName) }: QueryDTO): string => `
${niPluralize(
entityName
)}(queryParams: JSONObject, extendedInfo: Int, count: Int, offset: Int, search: String): [${returnableType}]
`;
export const getByIdQuery = ({ entityName, returnableType = upperFirst(entityName) }: QueryDTO): string => `
${entityName}ById(id: ID!): ${returnableType}
`;
export const getEntityDefaultQueries = ({ entityName, returnableType = upperFirst(entityName) }: QueryDTO): string => `
${getEntitiesQuery({ entityName, returnableType })}
${getByIdQuery({ entityName, returnableType })}
`;
export default getEntityDefaultQueries;

Typedefs implementation within the microservice:

const { gql } = require('apollo-server-express');const {
backOffice: {
queryTypes,
typeDefs: { Statistics, Scalars },
},
} = require('@naturalintelligence/shared-graphql');
const { ProductSchema } = require('./schemas');const typeDefs = gql`
${Scalars}
${Statistics} ${ProductSchema} extend type Query {
${queryTypes.getEntityDefaultQueries({ entityName: 'product' })}
}
`;
module.exports = {
typeDefs,
};

So, what are the results?

  • Payload size dramatically decreased.
  • The number of client-side requests decreased.
  • The developer’s velocity increased.
  • Schema safeness increased.

Conclusions

When I first encountered GraphQL I was afraid of using it because it wasn’t a standard, it was a new technology at the time. But a few years later it became clear that choosing GraphQL is the way to improve our system.

Following the steps described within this article, you will be able to create your own infrastructure, the same as I did, to improve the development speed, assist developers to add new entities to GraphQL, and increase your system quality.

“Good architecture is key to building modular, scalable, maintainable, and testable applications”.

Great thanks to Natural Intelligence which gave me the place and all the resources to make a successful improvement project.

Happy coding :)

--

--