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 currenttodoArray. - 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
forEachand 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 inpackage.jsonto 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.



