Scaling Next.js With Nx: Monorepos, Shared Code, And Smarter Builds

Nx is an open-source build framework designed to help teams architect, test, and build applications at scale. It provides a robust CLI, advanced caching, and dependency management, along with plugins for modern frameworks like Next.js. For developers working across multiple applications or looking to share reusable components, Nx introduces a structured monorepo workspace that houses source code and libraries for several projects in one place.

Beyond the monorepo setup, Nx brings several practical benefits to a Next.js workflow:

  • Graph-based task execution: Nx schedules tasks using a graph model to determine which application should run what, optimizing execution time across the workspace.
  • Built-in testing tools: Unit and end-to-end tests are preconfigured for your applications.
  • Computation caching: Nx tracks file changes since the last commit, so it can reanalyze only modified files and run builds or tests against just those changes—a major win for large codebases.
  • Visual dependency graph: Inspect how components and applications interact through a CLI-generated graph.
  • Code sharing: Create shared libraries once and use them across multiple apps, including between front-end and back-end projects.
  • Cloud integration: Optional cloud storage and GitHub integration let you share build logs and results with your team.

Publishable Libraries And Workspace Setup

Nx distinguishes between buildable and publishable libraries. Use the --buildable flag for libraries consumed only inside the monorepo. For components destined for an external registry—such as an organizational UI library built with Storybook—generate with the --publishable flag. You can then invoke a build with a command like nx build mylib, which outputs an optimized bundle to dist/mylib ready for deployment.

To start fresh with a Next.js preset, the CLI scaffolds a workspace with the app and a styling library preconfigured:

npx create-nx-workspace happynrwl \
--preset=next \
--style=styled-components \
--appName=todo

If you already have an Nx workspace, adding a Next.js application is done with:

npx nx g @nrwl/next:app

Creating The Workspace And A CRUD API

The Nx plugin for Next.js includes executors for serving and building applications. Generate a new workspace using the next preset:

npx create-nx-workspace happynrwl \
--preset=next \
--style=styled-components \
--appName=todo

You'll be prompted about Nx Cloud; for local development, decline. Once dependencies install, the generated tree places the Next.js app todo under apps, complete with a preconfigured E2E test. Run the frontend with npx nx serve todo.

To demonstrate monorepo support, the API server is added as a separate application within the same workspace. Install the Express plugin:

npm install --save-dev @nrwl/express

Generate the Express application, specifying its name and associating the frontend project that will consume it:

npx nx g @nrwl/express:application --name=todo-api --frontendProject=todo

The result is a todo-api folder under apps with a standard main.ts entry file. Inside this file, define an initial array of to-do objects with item and id keys, then add the CRUD routes:

/**
 * This is not a production server yet!
 * This is only minimal back end to get started.
 */
import * as express from 'express';
import {v4 as uuidV4} from 'uuid';

const app = express();
app.use(express.json()); // used instead of body-parser

let todoArray: Array<{ item: string; id: string }> = [
  { item: 'default todo', id: uuidV4() },
];
…
  • Read: The app.get() route returns the current todoArray.
  • Create: Accept a string value, and generate a new ID by incrementing the ID from the last array element.
  • Update: Accept the item's ID and a new value; loop through the array with forEach and update where the ID matches.
  • Delete: Accept an ID, filter out the matching item, and reassign the filtered array to todoArray.

The Next.js app folder contains a proxy.conf.json file that routes all API calls matching /api to the Express server:

{
  "/api": {
    "target": "http://localhost:3333",
    "secure": false
  }
}

Building The Frontend Flow

With the backend in place, generate a new page and a reusable component using the Nx CLI. After running the page generator, you'll select a styling library—styled-components works well here:

npx nx g @nrwl/next:page home

Create a todo-item component to represent each to-do entry. The component receives asynchronous callback props from the home page for edit and delete actions. The edit flow uses an isEditingItem state: the input is disabled when false; clicking "Edit" toggles it to true, enabling the input and showing an "Update" button that calls the passed updateItem function.

The home page holds the CRUD logic:

 …
  const [items, setItems] = useState<Array<{ item: string; id: string }>>([]);
  const [newItem, setNewItem] = useState<string>('');
  const fetchItems = async () => {
    try {
      const data = await fetch('/api/fetch');
      const res = await data.json();
      setItems(res.data);
    } catch (error) {
      console.log(error);
    }
  };
  const createItem = async (item: string) => {
    try {
      const data = await fetch('/api', {
        method: 'POST',
        body: JSON.stringify({ item }),
        headers: {
          'Content-Type': 'application/json',
        },
      });
    } catch (error) {
      console.log(error);
    }
  };
  const deleteItem = async (id: string) => {
    try {
      const data = await fetch('/api', {
        method: 'DELETE',
        body: JSON.stringify({ id }),
        headers: {
          'Content-Type': 'application/json',
        },
      });
      const res = await data.json();
      alert(res.message);
    } catch (error) {
      console.log(error);
    }
  };
  const updateItem = async (id: string, updatedItem: string) => {
    try {
      const data = await fetch('/api', {
        method: 'PATCH',
        body: JSON.stringify({ id, updatedItem }),
        headers: {
          'Content-Type': 'application/json',
        },
      });
      const res = await data.json();
      alert(res.message);
    } catch (error) {
      console.log(error);
    }
  };
  useEffect(() => {
    fetchItems();
  }, []);
…

These functions handle fetching all items, creating a new item from a string, updating an item by its ID with a new value, and deleting an item by ID. To render, the component maps over the items state and displays the list:

 …
return (
    <StyledHome>
      <h1>Welcome to Home!</h1>
      <TodoWrapper>
         {items.length > 0 &&
          items.map((val) => (
            <TodoItem
              key={val.id}
              item={val.item}
              id={val.id}
              deleteItem={deleteItem}
              updateItem={updateItem}
              fetchItems={fetchItems}
            />
          ))}
      </TodoWrapper>
      <form
        onSubmit={async(e) => {
          e.preventDefault();
          await createItem(newItem);
          //Clean up new item
          setNewItem('');
          await fetchItems();
        }}
      >
        <FlexWrapper>
          <Input
            value={newItem}
            onChange={({ target }) => setNewItem(target.value)}
            placeholder="Add new item…"
          />
          <Button success type="submit">
            Add +
          </Button>
        </FlexWrapper>
      </form>
    </StyledHome>
  );
…

With the server running via npx nx serve todo-api and the Next.js app via npx nx serve todo, the default to-do item appears in the browser, and you can interact with the full CRUD cycle.

Useful Nx Commands And Visualizing The Graph

Nx offers several high-level CLI commands for day-to-day work:

  • nx list — Shows installed Nx plugins.
  • nx migrate latest — Updates packages in package.json to their latest versions.
  • nx affected — Targets tasks only to modified apps.
  • nx run-many --target serve --projects todo-api,todo — Runs a target across multiple listed projects.

To inspect the workspace architecture, run npx nx dep-graph. This opens a visual representation in the terminal showing exactly how the Next.js frontend, Express API, and any shared libraries reference one another.