August 23rd, 2023

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:

  1. Generate the necessary codegen configuration in your project with nx g @nxify-io/graphql-codegen:configuration
  2. Run the codgen target for the current project to generate the graphql function in the src/lib/generated directory
  3. Create a component and define its data requirements as a GraphQL fragment
  4. Run the codegen target again for the current project to generate the expected type definitions

You can add the watch option to the codegen 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.

Kennie Davis

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.