Blog
Philip Fulcher
February 21, 2025

Using Apollo GraphQL in an Nx Workspace

Using Apollo GraphQL in an Nx Workspace

Because of the Nx's robust support for a diverse ecosystem of JavaScript development, it enables you to build your entire full-stack application in a single repo. This allows you to share code and interfaces between your frontend and backend and acts as a multiplier on your development velocity.

GraphQL is a query language for your API. Because of its typed schema definition, it’s a great candidate for defining the contract between your API and its consumers. By using smart tools to generate code based on that schema, you can save yourself a lot of time and enforce better cooperation between your frontend and backend.

In this article, you will build a simple GraphQL API that tracks some information about Lego sets. You’ll create this API using Apollo Server, and it will be consumed by a React application. You’ll have this all inside of an Nx Workspace in a single repository.

In this article, you’ll learn how to:

  • Create an Nx workspace for both frontend and backend applications
  • Create a GraphQL API using Apollo Server
  • Generate frontend code and backend resolver types based on your GraphQL schema using GraphQL Codegen
  • Create a React application to consume your GraphQL API
Minimal configuration

When given the option to enable another tool, like linting or testing, we're going to decline. This keeps this article focussed on GraphQL instead of having lint and test configs in the example. As you progress, feel free to enable these additional options if you'd like, especially if you're adding to an existing workspace that has those tools enabled.

An example repo with all the work you’ll be doing here can be found in our Nx Recipes repo

Create a new workspace

Start by creating an Nx workspace:

npx create-nx-workspace@latest --preset=node-monorepo nx-apollo

When prompted, answer the prompts as follows:

❯ npx create-nx-workspace@latest --preset node nx-apollo

NX Let's create a new workspace [https://nx.dev/getting-started/intro]

✔ Application name · api

✔ What framework should be used? · none

✔ Would you like to generate a Dockerfile? [https://docs.docker.com/] · No

✔ Which CI provider would you like to use? · skip

✔ Would you like remote caching to make your build faster? · skip

Create GraphQL schema and project

We want to generate model types from our schema that can be used by other projects in our workspace, so we'll start by creating a new project:

npx nx g @nx/js:library --directory=libs/models-graphql models-graphql

When prompted, answer the prompts as follows:

❯ npx nx g @nx/js:library --directory=libs/models-graphql models-graphql

NX Generating @nx/js:library

✔ Which bundler would you like to use to build the library? Choose 'none' to skip build setup. · none

✔ Which linter would you like to use? · none

✔ Which unit test runner would you like to use? · none

Why do I need a separate project?

When you have your GraphQL schema and generated models in a separate project, other projects can depend on it. This ensures that all projects are using the same version of the schema and models. This exemplifies the "modulith" structure for monorepos. Read more

You need a GraphQL schema to create the API, so write a very simple one with a single query and a single mutation. Create a file named schema.graphql in the new models-graphql project:

libs/models-graphql/src/lib/schema.graphql
1type Set { 2 id: Int! 3 name: String 4 year: Int 5 numParts: Int 6} 7 8type Query { 9 allSets: [Set] 10} 11 12type Mutation { 13 addSet(name: String, year: String, numParts: Int): Set 14} 15

To start generating our models from this schema, we'll use GraphQl Codegen. Install the packages needed:

npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers @graphql-codegen/typescript-react-apollo

GraphQL Codegen is controlled by a configuration file named codegen.ts in each project that needs it. Create one for models-graphql:

libs/models-graphql/codegen.ts
1import type { CodegenConfig } from '@graphql-codegen/cli'; 2 3const config: CodegenConfig = { 4 schema: 'libs/models-graphql/src/lib/schema.graphql', 5 generates: { 6 'libs/models-graphql/src/lib/__generated__/models.ts': { 7 plugins: ['typescript'], 8 config: { 9 avoidOptionals: true, 10 }, 11 }, 12 }, 13}; 14export default config; 15

To run GraphqlQL Codegen, we need a target added to our project. Add this to the project.json for models-graphql:

libs/models-graphql/project.json
1"targets": { 2 "codegen": { 3 "command": "npx graphql-codegen --config {projectRoot}/codegen.ts" 4 } 5} 6

Run the codegen task to generate our new models:

npx nx codegen models-graphql

You should see the new models created in the __generated__ directory in models-graphql. To use them outside the project, export them from the index.ts:

libs/models-graphql/src/index.ts
1export * from './lib/__generated__/models'; 2

Create GraphQL API

Use Apollo Server to create your GraphQL api. Start by installing the GraphQL modules needed for Apollo

npm install @apollo/server graphql

The workspace was generated with a Node application for us, but we need to make some small changes to support ESM for Apollo Server. First, change these compiler options in tsconfig.app.json:

apps/api/tsconfig.app.json
1 "compilerOptions": { 2 "lib": ["es2020"], 3 "target": "es2020", 4 "module": "esnext", 5 "moduleResolution": "node", 6 "esModuleInterop": true, 7 ... 8 } 9

And change the build target config in project.json:

apps/api/project.json
1"targets": { 2 "build": { 3 ... 4 "options": { 5 ... 6 "format": ["esm"] 7 } 8 } 9} 10

GraphQl Codegen has already created our models for our GraphQL schema, but it can also generate the resolver types we'll want to implement in Apollo Server. Like before, create a codegen.ts in the api application:

apps/api/codegen.ts
1import type { CodegenConfig } from '@graphql-codegen/cli'; 2 3const config: CodegenConfig = { 4 schema: 'libs/models-graphql/src/lib/schema.graphql', 5 generates: { 6 'apps/api/src/__generated__/resolvers.ts': { 7 plugins: ['add', 'typescript-resolvers'], 8 config: { 9 useIndexSignature: true, 10 content: 'import * as types from "@nx-apollo/models-graphql"', 11 namespacedImportName: 'types', 12 }, 13 }, 14 }, 15}; 16export default config; 17

And add the task to our targets:

apps/api/project.json
1 "targets": { 2 "codegen": { 3 "command": "npx graphql-codegen --config {projectRoot}/codegen.ts" 4 } 5} 6

And run the task:

npx nx codegen api

And now there should be resolver types in the __generated__ directory. We're ready to put create our Apollo Server application. Replace the contents of main.ts with this:

apps/api/src/main.ts
1import { ApolloServer } from '@apollo/server'; 2import { startStandaloneServer } from '@apollo/server/standalone'; 3import { Set } from '@nx-apollo/models-graphql'; 4import { readFileSync } from 'fs'; 5import { join } from 'path'; 6import { Resolvers } from './__generated__/resolvers'; 7 8// Note: this uses a path relative to the project's 9// root directory, which is the current working directory 10// if the server is executed using `npm run`. 11const typeDefs = readFileSync( 12 join('libs/models-graphql/src/lib', 'schema.graphql'), 13 { encoding: 'utf-8' } 14); 15 16const sets: Set[] = [ 17 { 18 id: 1, 19 name: 'Voltron', 20 numParts: 2300, 21 year: '2019', 22 }, 23 { 24 id: 2, 25 name: 'Ship in a Bottle', 26 numParts: 900, 27 year: '2019', 28 }, 29]; 30 31// Resolvers define how to fetch the types defined in your schema. 32// This resolver retrieves books from the "books" array above. 33const resolvers: Resolvers = { 34 Query: { 35 allSets: () => sets, 36 }, 37 Mutation: { 38 addSet: (parent, args) => { 39 const newSet = { 40 id: sets.length + 1, 41 name: args.name, 42 year: args.year, 43 numParts: +args.numParts, 44 }; 45 46 sets.push(newSet); 47 48 return newSet; 49 }, 50 }, 51}; 52 53// The ApolloServer constructor requires two parameters: your schema 54// definition and your set of resolvers. 55const server = new ApolloServer({ 56 typeDefs, 57 resolvers, 58}); 59 60// Passing an ApolloServer instance to the `startStandaloneServer` function: 61// 1. creates an Express app 62// 2. installs your ApolloServer instance as middleware 63// 3. prepares your app to handle incoming requests 64const { url } = await startStandaloneServer(server, { 65 listen: { port: 4000 }, 66}); 67 68console.log(`🚀 Server ready at: ${url}`); 69

This is already enough to see some progress when you run the api application.

npx nx serve api

When the application is running, bring up the GraphQL Playground in your browser at http://localhost:4000/

Here you can inspect your GraphQL schema as well as submit queries and mutations.

This is a very simple resolver that holds data in memory. It returns the current contents of the sets array for the allSets query and allows users to add a new set using the addSet mutation. Add this resolver to the providers array in your app module:

Go back to your GraphQL Playground and see if your queries return any data now. Try a query and a mutation:

1query allSets { 2 allSets { 3 id 4 name 5 numParts 6 } 7} 8 9mutation addSet { 10 addSet(name: "My New Set", numParts: 200, year: "2020") { 11 id 12 } 13} 14

Now that the API is working, you’re ready to build a frontend to access this.

Add React frontend

Start by adding a React app to your workspace using the @nx/react plugin:

npx nx add @nx/react

Create the React app using the generator:

npx nx g @nx/react:app --directory=apps/frontend frontend

When prompted, answer the prompts as follows:

❯ npx nx g @nx/react:app --directory=apps/frontend frontend

NX Generating @nx/react:application

✔ Which stylesheet format would you like to use? · tailwind

✔ Would you like to add React Router to this application? (y/N) · false

✔ Which bundler do you want to use to build the application? · vite

✔ Which linter would you like to use? · none

✔ What unit test runner should be used? · none

✔ Which E2E test runner would you like to use? · none

We use Tailwind styles here for convenience. It will allow us to add some simple styles to our app without adding CSS files and importing them.

The Apollo client makes it easy to consume your GraphQL API. Install the client:

npm install @apollo/client

Modify your app.tsx to provide the Apollo Client:

apps/frontend/src/main.tsx
1import { StrictMode } from 'react'; 2import * as ReactDOM from 'react-dom/client'; 3import App from './app/app'; 4import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; 5 6const root = ReactDOM.createRoot( 7 document.getElementById('root') as HTMLElement 8); 9 10const client = new ApolloClient({ 11 uri: 'http://localhost:4000/graphql', 12 cache: new InMemoryCache(), 13}); 14 15root.render( 16 <StrictMode> 17 <ApolloProvider client={client}> 18 <App /> 19 </ApolloProvider> 20 </StrictMode> 21); 22

Create React library

Nx helps you break down your code into well-organized libraries for consumption by apps, so create a couple of React libraries to organize your work. Create a data-access library that handles communication with the backend and a feature-sets library that includes container components for displaying the Lego set data. In a real app, you might also create a ui library that includes reusable presentational components, but that is not part of this example. For more information on how to organize your React monorepo using Nx, read our book Effective React Development with Nx by registering here

To create the described project, run this command:

npx nx g @nx/react:lib --directory=libs/feature-sets feature-sets

When prompted, answer the prompts as follows:

npx nx g @nx/react:lib --directory=libs/feature-sets feature-sets

NX Generating @nx/react:library

✔ Which bundler would you like to use to build the library? Choose 'none' to skip build setup. · none

✔ What unit test runner should be used? · none

Setup React Code Generation

A tool called GraphQL Codegen makes the development of your feature library faster.

You need to create some GraphQL queries and mutations for the frontend to consume. Create a file named operations.graphql in the projects:

libs/feature-sets/src/lib/operations.graphql
1mutation addSet($name: String!, $year: String!, $numParts: Int!) { 2 addSet(name: $name, year: $year, numParts: $numParts) { 3 id 4 name 5 numParts 6 year 7 } 8} 9 10query setList { 11 allSets { 12 id 13 name 14 numParts 15 year 16 } 17} 18

Once again create a codegen.ts for the project:

libs/feature-sets/codegen.ts
1import { CodegenConfig } from '@graphql-codegen/cli'; 2 3const config: CodegenConfig = { 4 schema: 'libs/models-graphql/src/lib/schema.graphql', 5 documents: ['libs/feature-sets/src/**/*.graphql'], 6 generates: { 7 'libs/feature-sets/src/lib/__generated__/operations.ts': { 8 plugins: ['add', 'typescript-operations', 'typescript-react-apollo'], 9 config: { 10 useIndexSignature: true, 11 content: 'import * as types from "@nx-apollo/models-graphql"', 12 namespacedImportName: 'types', 13 }, 14 }, 15 }, 16 ignoreNoDocuments: true, 17}; 18 19export default config; 20

This configuration grabs all of your GraphQL files and generates all the needed types and services to consume the API.

Add a new task in project.json to run this code generator:

libs/feature-sets/project.json
1 "targets": { 2 "codegen": { 3 "command": "npx graphql-codegen --config {projectRoot}/codegen.ts" 4 } 5} 6

Now you can run that task using the Nx CLI:

npx nx codegen feature-sets

You should now have a folder called __generated__ in your feature-sets library with a file named operations.ts. It contains typing information about the GraphQL schema and the operations you defined. It even has some hooks that make consuming this API superfast.

Create React components

You now have everything needed to start building your React components. Create two components: a list of Lego sets and a form to add a Lego set. Use Nx generators to scaffold these:

npx nx generate @nx/react:component libs/feature-sets/src/lib/add-set-form

npx nx generate @nx/react:component libs/feature-sets/src/lib/set-list

When prompted, answer the prompts as follows:

❯ npx nx generate @nx/react:component libs/feature-sets/src/lib/add-set-form

NX Generating @nx/react:component

✔ Should this component be exported in the project? (y/N) · false

In the SetList component, add the following:

libs/feature-sets/src/lib/set-list.tsx
1import { useSetListQuery } from './__generated__/operations'; 2 3export function SetList() { 4 const { loading, data } = useSetListQuery(); 5 6 return loading ? ( 7 <p>Loading ...</p> 8 ) : ( 9 <ul className="mx-6 w-full list-none"> 10 {data && 11 data.allSets?.map(({ id, name, numParts, year }) => ( 12 <li className="p-2 even:bg-slate-200"> 13 {year} - <strong>{name}</strong> ({numParts} parts) 14 </li> 15 ))} 16 </ul> 17 ); 18} 19 20export default SetList; 21

Notice how useSetListQuery is imported from the data-access library. This is a hook generated by GraphQL Codegen that provides the results of the SetList query. This entire pipeline is type-safe, using the types generated by GraphQL Codegen.

In the AddSetForm component, add the following:

libs/feature-sets/src/lib/add-set-form.tsx
1import { useRef } from 'react'; 2import { useAddSetMutation } from './__generated__/operations'; 3 4export function AddSetForm() { 5 const formRef = useRef<HTMLFormElement>(null); 6 const [addSet] = useAddSetMutation({ 7 refetchQueries: ['setList'], 8 }); 9 10 const handleSubmit = (formData: FormData) => { 11 const name = formData.get('name')?.toString(); 12 const year = formData.get('year')?.toString(); 13 const numParts = parseInt(formData.get('numParts')?.toString() || '0', 10); 14 15 if (name && year && numParts > 0) { 16 addSet({ variables: { name, year, numParts } }); 17 } 18 formRef.current?.reset(); 19 }; 20 21 return ( 22 <form 23 ref={formRef} 24 action={handleSubmit} 25 className="mx-6 max-w-60 border border-slate-200 p-6" 26 > 27 <label 28 htmlFor="name" 29 className="block text-sm/6 font-medium text-gray-900" 30 > 31 Name 32 </label> 33 <div className="mt-2"> 34 <input 35 id="name" 36 name="name" 37 type="text" 38 className="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6" 39 /> 40 </div> 41 42 <label 43 htmlFor="name" 44 className="mt-2 block text-sm/6 font-medium text-gray-900" 45 > 46 Year 47 </label> 48 <div className="mt-2"> 49 <input 50 id="year" 51 name="year" 52 type="text" 53 className="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6" 54 /> 55 </div> 56 57 <label 58 htmlFor="name" 59 className="mt-2 block text-sm/6 font-medium text-gray-900" 60 > 61 Number of Parts 62 </label> 63 <div className="mt-2"> 64 <input 65 id="numParts" 66 name="numParts" 67 type="number" 68 className="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6" 69 /> 70 </div> 71 72 <button 73 type="submit" 74 className="mt-6 rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" 75 > 76 Create new set 77 </button> 78 </form> 79 ); 80} 81 82export default AddSetForm; 83

Again, notice that the component imports hooks, queries, and typing information from our generated code to accomplish this.

Integrate components into the app

Final step: bring those new components into FeatureSets component:

libs/feature-sets/src/lib/feature-sets.tsx
1import AddSetForm from './add-set-form'; 2import SetList from './set-list'; 3 4export function FeatureSets() { 5 return ( 6 <div className="flex"> 7 <AddSetForm></AddSetForm> 8 <SetList></SetList> 9 </div> 10 ); 11} 12 13export default FeatureSets; 14

And bring that component into your app:

apps/frontend/src/app/app.tsx
1import { FeatureSets } from '@nx-apollo/feature-sets'; 2 3export function App() { 4 return ( 5 <div> 6 <h1 className="my-6 text-center text-2xl font-bold">My Lego Sets</h1> 7 <FeatureSets></FeatureSets> 8 </div> 9 ); 10} 11 12export default App; 13

If your API isn’t running already, go ahead and start it:

npx nx serve api

And now start your React app in a separate terminal:

npx nx serve frontend

Browse to http://localhost:4200 and see the results of your work!

Extend codegen configuration

The configuration for the codegen targets is a good start, but it's currently lacking two things:

  1. Caching
  2. Dependent tasks

Without caching enabled, codegen tasks will be run every time, regardless if they need to be or not. And without dependent tasks configured, we can't be sure that codegen is run any time our generated code depends on the generated code in another project. IOn our example, the generated code in both api and feature-sets rely on the models generated in models-graphql. If we make changes to the schema in models-graph and then run codegen on api, our models will be out-of-sync and lead to errors.

Let's fix both of these problems with a target default for codegen. In nx.json, add this:

nx.json
1 "targetDefaults": { 2 ... 3 "codegen": { 4 "cache": true, 5 "outputs": ["{projectRoot}/src/__generated__"], 6 "inputs": ["{workspaceRoot}/libs/models-graphql/src/lib/schema.graphql","{projectRoot}/**/*.graphql"], 7 "dependsOn": ["^codegen"] 8 } 9 } 10

Now try running codegen for api to see that codegen for models-graphql is run first:

❯ npx nx codegen api

✔ 1/1 dependent project tasks succeeded [0 read from cache]

Hint: you can run the command with --verbose to see the full dependent project outputs

—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

> nx run api:codegen

> npx graphql-codegen --config apps/api/codegen.ts

✔ Parse Configuration

✔ Generate outputs

—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

NX Successfully ran target codegen for project api and 1 task it depends on (2s)

Try running the command again, and you'll see that the results are pulled from the cache, and the task ends immediately.

Further Reading