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
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
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:
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
:
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
:
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
:
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
:
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
:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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.