Slim down resolvers with loaders
GraphQL runtimes may grow over time: resolvers will have more and more code to provide the required features. In Front-Commerce's core and platforms integrations, we have introduced the concept of 'loaders' to handle this complexity, keep code maintainable and increase testability.
This page explains what loaders are and how you can create them. This will also help you better understand the Front-Commerce core code.
Loaders are an abstraction used in Front-Commerce to group all the business logic of a module.
Why loaders?
When resolvers grow, you will likely extract functions or modules to share code
between resolvers. It can be: constructing an axios
instance with the correct
URL and headers, modifying responses to return objects matching the GraphQL
schema or error management for instance.
Loaders are aimed at being node modules encapsulating all your domain/API specific code. We recommend to design them as pure JS modules without any dependency on Front-Commerce or GraphQL features. Designing loaders with this approach has several advantages:
- you can reuse existing code or libraries from another project
- if you migrate away from Front-Commerce, this code could be reused easily in your new application (as long as it is in a Javascript environment of course)
- testability: the code from both your resolvers and loaders would be easier to test. You could inject fakes or mock loaders to test resolvers, and you could test loaders without the complexity of a GraphQL context since they are pure JS modules.
What are loaders?
In short: loaders can be anything you feel relevant to your use case.
We recommend to use patterns that your team understands: classes, pure functions or a mix between both approaches. It could be in-memory data transformation as well as data fetching code.
The most important is to make it independent from the infrastructure (express server, GraphQL implementation…) by injecting parameters or configuration extracted from the environment (request or server configuration) where relevant.
In Front-Commerce’s core GraphQL module, you will often find loaders exposing a repository-like interface. Here is for instance the functions exposed by Magento2’s product loader:
load(sku: string)
loadBySkus(skus: array)
loadByCategory(categoryId: int, paginationCriteria: object)
countInCategory(categoryId: int)
loadUpsellsOf(sku: string, paginationCriteria: object)
loadRelatedOf(sku: string, paginationCriteria: object)
loadCrosssellsOf(sku: string, paginationCriteria: object)
Each function is responsible for calling Magento’s REST API (with error management and headers corresponding to the current user) and formatting results according to the GraphQL schema.
When prototyping or designing your GraphQL module, we have found it convenient to start with fake implementations of loaders returning hardcoded static data. It allows to have an executable schema earlier, that could serve as a starting point for both frontend and backend developers.
In a project, you could also start with a simple solution (such as reading data from a local CSV file) and add more advanced features (such as an admin interface to manage these data) later in the project. Loaders would then be the only part of your code to adapt to this complexity.
Refactoring to loaders
We will go through the process of extracting business logic from resolvers to a loader and discover the different tools provided by Front-Commerce to achieve this.
Initial resolvers
For this example, we will reuse loaders from the example used in the Extend the GraphQL schema guide.
Here is the initial code we will refactor:
import { createGraphQLRuntime } from "@front-commerce/core/graphql";
export default createGraphQLRuntime({
resolvers: {
Product: {
clickCounter: (product) => {
const clickCountPromise = fetchClickCountForSku(product.sku);
return clickCountPromise;
},
},
Mutation: {
incrementProductCounter: async (_root, params) => {
const { sku, incrementValue } = params;
try {
await incrementProductCount(sku, incrementValue);
return {
success: true,
};
} catch (error: unknown) {
return {
success: false,
errorMessage: error.message,
};
}
},
},
},
});
Extract code in a loader
Let’s start by creating a new loader.js
file with our loader module.
export default {
loadBySku: (product) => {
const clickCountPromise = fetchClickCountForSku(product.sku);
return clickCountPromise;
},
incrementBySku: async (params) => {
const { sku, incrementValue } = params;
try {
await incrementProductCount(sku, incrementValue);
return {
success: true,
};
} catch (error: unknown) {
return {
success: false,
errorMessage: error.message,
};
}
},
};
You can then update the resolvers to use this loader:
import { createGraphQLRuntime } from "@front-commerce/core/graphql";
+ import CounterLoader from "./loader"
export default createGraphQLRuntime({
resolvers: {
Product: {
- clickCounter: (product) => {
- const clickCountPromise = fetchClickCountForSku(product.sku);
- return clickCountPromise;
- },
+ clickCounter: (product) => CounterLoader.loadBySku(product)
},
Mutation: {
incrementProductCounter: async (_root, params) => {
- const { sku, incrementValue } = params;
- try {
- await incrementProductCount(sku, incrementValue);
- return {
- success: true,
- };
- } catch (error: unknown) {
- return {
- success: false,
- errorMessage: error.message,
- };
- }
+ return CounterLoader.incrementBySku(params)
},
},
},
});
Using GraphQL context for Dependency Injection
To improve the design of resolvers, we recommend leveraging
GraphQL's context
for dependency injection by using
contextEnhancer
.
It allows loaders to be injected into resolvers instead of being tightly coupled
to the current implementation.
The GraphQL context, which is provided to every resolver and holds important contextual information like the currently logged-in user or access to a database, is passed as the third argument of a resolver function in Front-Commerce and most GraphQL implementations. This enables a cleaner approach where resolvers can utilize loaders from the context, enhancing modularity and maintainability.
import { createGraphQLRuntime } from "@front-commerce/core/graphql";
import CounterLoader from "./loader";
export default createGraphQLRuntime({
+ contextEnhancer: (params) => {
+ return {
+ Counter: CounterLoader,
+ };
+ },
resolvers: {
Product: {
- clickCounter: (product) => CounterLoader.loadBySku(product),
+ clickCounter: (product, _, { loaders }) => {
+ return loaders.Counter.loadBySku(product);
+ }
},
Mutation: {
- incrementProductCounter: async (_root, params) => {
+ incrementProductCounter: async (_root, params, { loaders }) => {
- return CounterLoader.incrementBySku(params);
+ return loaders.Counter.incrementBySku(params);
},
},
},
});
Supporting advanced use cases
The refactoring explained above enables a wide range of use cases. We encourage you to get used to this pattern and explore the following documentation pages to see how you could combine them to achieve your goals.
The content below is currently being written. If you need more detailed information, please contact us. We will make sure to answer you in a timely manner.
- optimize remote data fetching with dataloaders
- interacting with Magento2 REST API
- accessing the user session
- using GraphQL modules dependencies to extend an existing loader
Let’s now take a look at a real-world module definition as an example.
Example from the core
Front-Commerce itself is written as GraphQL modules. You can browse Front-Commerce’s source code to find more examples and understand how it works.
Here is for instance how Magento2 CMS runtime's definition looks like. You should see several patterns mentioned previously and get ideas about applying them in your application:
import { createGraphQLRuntime } from "@front-commerce/core/graphql";
import {
makeUserClientFromRequest,
makeUserGraphQLClientFromRequest,
} from "../core/factories";
import resolvers from "./resolvers"; // Our resolvers
import { CmsBlockLoader } from "./loaders"; // A first loader
import CmsPageLoader from "./loaders/CmsPage"; // A second loader
export default createGraphQLRuntime({
resolvers,
contextEnhancer: ({ req, loaders, makeDataLoader }) => {
const axiosInstance = makeUserClientFromRequest(req);
const axiosGraphQLInstance = makeUserGraphQLClientFromRequest(req);
const CmsPages = new CmsPageLoader(axiosGraphQLInstance, makeDataLoader);
return {
CmsPages,
CmsBlocks: CmsBlockLoader(makeDataLoader)(axiosInstance, loaders.Store),
};
},
});
As you can see, the contextEnhancer
is a bridge between the infrastructure
layer (request, caching strategies, configuration) and loaders. It creates
loader instances that could then be reused in resolvers, hiding Magento’s
specific concerns from them… leading to code that is easier to understand and
maintain!