Add GraphQL Code Generation to an Nx Workspace
Consuming a GraphQL endpoint in a TypeScript project adds the additional overhead of generating type definitions for the schema, queries, operations, etc. The GraphQL Code Generator is a fantastic tool to automate this process and it can be added to an Nx workspace with relative ease, especially with the @nxify-io/graphql-codegen
plugin.
I enjoy working with GraphQL almost as much as I do Nx.
A common pain point when consuming a GraphQL endpoint in a TypeScript project is adding type definitions for the schema, queries, operations, etc. The GraphQL Code Generator is a fantastic solution to this tedium and the client preset makes the task even easier. However, there is some nuance to getting everything working in an Nx workspace especially if you need to generate types across multiple projects.
Fragment Masking and the Client Preset
Before digging into my solution, I want to add some additional context. When I set out to build my personal blog in an effort to learn the NextJS App Router and Server Components, I knew I'd be using the Hygraph CMS and thus GraphQL, and in brushing up on code generation, I came across this fantastic article: Unleash the power of Fragments with GraphQL Codegen.
Immediately intrigued, I decided to give Fragment Masking a try as it seemed like a natural fit in an Nx environment where we split application code across logical boundaries and compose them together as needed. I settled on the following project structure:
apps/
kjd/
libs/
kjd/
feature-blog/
feature-main/
ui-fragments/
My goal was to define component data requirements as GraphQL fragments colocated with the component component code in the ui-fragments
project. The feature-blog
and feature-main
libraries are then responsible for defining the final queries, managing data access, and exporting page components to the kjd
app. This conceptually allows each page.tsx
file to act as route configuration, i.e., the home page is as simple as:
import { HomePage, homePageMetadataGenerator } from '@nxify/kjd-feature-home';
export const generateMetadata = homePageMetadataGenerator;
export const revalidate = 10;
export default HomePage;
Here, all page and metadata logic is maintained in @nxify/kjd-feature-home
allowing only route specific configuration like revalidation
to be maintained in the app. This is more relevant for dynamic routes that may share core page logic while requiring different revalidation times, metadata generation, or even static generation.
Code Generation Across Libraries
My initial hunch was that executing graphql-codegen
for a library with dependencies should work the same way TailwindCSS works in Nx relying on the createGlobPatternsForDependencies
utility.
To explain, here is a default tailwind.config.js
file generated by Nx:
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
const { join } = require('path');
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
join(
__dirname,
'{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}'
),
...createGlobPatternsForDependencies(__dirname),
],
…
};
The content
array instructs Tailwind where to find all utility class names and createGlobPatternsForDependencies
ensures that glob patterns for each dependency in the workspace are included. By comparison, here is a condegen.ts
file using the same strategy:
import { CodegenConfig } from '@graphql-codegen/cli';
import { createGlobPatternsForDependencies } from '@nx/js/src/utils/generate-globs';
const config: CodegenConfig = {
documents: [
...createGlobPatternsForDependencies(__dirname, 'lib/{client,server}/**/*!(*.stories|*.spec).{ts,tsx}'),
'libs/kjd/ui-fragments/src/lib/{client,server}/**/*!(*.stories|*.spec).{ts,tsx}',
],
…
};
export default config;
Here, the documents
array instructs the GraphQL Code Generator where to find all of the GraphQL artifacts and, thanks to Nx, includes glob patterns for each dependency.
Composing Fragments into Pages
You can review a complete code example in my Nx workspace; here I’m going to provide a brief summary of how I define and compose fragments.
Type Fragments
Each fragment component defines its data needs as a TypedDocumentNode
and subsequently “unmasks” that fragment within the component.
import { SectionLayout } from '@nxify/kjd-ui-layout';
import { FragmentType, fragmentData, graphql } from '../../generated';
export const ArticleAuthorFragment = graphql(`
fragment ArticleAuthorFragment on Article {
author {
name
avatar {
url
}
biography
}
}
`);
export interface ArticleAuthorProps {
data: FragmentType<typeof ArticleAuthorFragment>;
}
export function ArticleAuthor({ data }: ArticleAuthorProps) {
const { author } = fragmentData(ArticleAuthorFragment, data);
return (
…
);
}
The graphql
function is the magical utility responsible for generating each TypedDocumentNode
and it is actually generated by graphql-codegen
the first time you run the generator in a project. Each subsequent run of the code generator will regenerate your type definitions.
Query Fragments
Query fragments are then composed from type fragments such as this ArticleContent
component which builds a query from the ArticleHero
, ArticleMarkdown
, and ArticleAuthor
fragments.
import { notFound } from 'next/navigation';
import { FragmentType, fragmentData, graphql } from '../../generated';
import { ArticleHero } from '../article-hero/article-hero';
import { ArticleMarkdown } from '../article-markdown/article-markdown';
import { ArticleAuthor } from '../article-author/article-author';
export const ArticleContentQueryFragment = graphql(`
fragment ArticleContentQueryFragment on Query {
article(where: { slug: $slug }) {
...ArticleHeroFragment
...ArticleMarkdownFragment
...ArticleAuthorFragment
}
}
`);
export interface ArticleContentProps {
data: FragmentType<typeof ArticleContentQueryFragment>;
}
export function ArticleContent({ data }: ArticleContentProps) {
const { article } = fragmentData(ArticleContentQueryFragment, data);
if (!article) {
return notFound();
}
return (
<>
<ArticleHero data={article} />
<ArticleMarkdown data={article} />
<ArticleAuthor data={article} />
</>
);
}
Page Components
Finally, an ArticlePage
is defined in the @nxify/kjd-feature-blog
library and is responsible for constructing and executing the query and the query result is then passed to the fragments defined in @nxify/kjd-ui-fragments
.
import { graphql } from '../../generated';
import { hygraph } from '@nxify/kjd-data-access-hygraph';
import { ArticleContent } from '@nxify/kjd-ui-fragments';
import { Metadata } from 'next';
const ArticleQuery = graphql(`
query ArticleQuery($slug: String!) {
...ArticleContentQueryFragment
}
`);
export interface ArticlePageProps {
params: { slug: string };
}
export async function ArticlePage({ params }: ArticlePageProps) {
const query = await hygraph.request(ArticleQuery, { slug: params.slug });
return <ArticleContent data={query} />;
}
How, When, and Where to Run Codegen
This part is unfortunately a little cumbersome and to address that, I’ve wrapped everything up into an Nx plugin that will generate all of the configurations discussed below for you: @nxify-io/graphql-codegen
.
If you recall the original project structure I outlined, I have three libraries that require code generation: ui-fragments
, feature-blog
, and feature-home
. Each project needs a codegen.ts
file and have a codegen
target added to its project.json
file.
The codegen
target is pretty simple:
"codegen": {
"executor": "@nxify-io/graphql-codegen:codegen",
"options": {
"config": "{projectRoot}/codegen.ts"
},
"dependsOn": ["^codegen"]
}
The options
can be extended with additional GraphQL Code Generator options such as watch
and the dependOn: [“^codegen”]
ensures that code is generated across all projects together. You can easily run codegen
for a single project or nx affected –target=codegen
to regenerate everything.
Remember that the magical graphql
function discussed earlier has to be defined before you can start generating typed fragments. Thus, the typical workflow is:
- Generate the necessary codegen configuration in your project with
nx g @nxify-io/graphql-codegen:configuration
- Run the
codgen
target for the current project to generate thegraphql
function in thesrc/lib/generated
directory - Create a component and define its data requirements as a GraphQL fragment
- Run the
codegen
target again for the current project to generate the expected type definitions
You can add the
watch
option to thecodegen
target to regenerate type definitions automatically, just remember to remove the option when you’re done.
A Note on Caching
The @nxify-io/graphql-codegen
plugin does not support Nx caching by default. The code generator reads the schema and updates local type definitions regardless of any changes in your client code. To ensure that these external changes are always fetched, caching is disabled. A custom hasher may be able to address this although I haven't spent any time investigating that yet.
If your setup can benefit from caching, you can simply add codegen
to the list of cacheableOperations
in your nx.json
file at the root of your workspace.
Wrapping Up
There is quite a lot of information to cover on this topic and I’ve only scratched the surface. If you use Nx and can benefit from GraphQL Code Generation, please check out the @nxify-io/graphql-codegen
plugin. Feedback is a gift and PRs are always welcome to add, extend, or correct functionality.
Hi, I'm Kennie Davis
An Arizona native, father of three, and software engineer passionate about Nx, React, GraphQL and improving both the developer and user experience. I write about what I'm working on and learning along the way.