I've got a few related projects that used to live in separate repos, each with its own duplicated config, its own copy of shared components, its own version drift. Every time I fixed a bug in one, I had to remember to port the fix everywhere else. Nx kept coming up as the answer, so I finally sat down and built a workspace instead of reading about it.
npx create nx workspace@latest scaffolds a single repo that holds multiple apps and libraries, each with its own project.json describing its targets (build, test, lint, whatever). The part that I like is Nx builds a dependency graph of every project by reading actual import statements, not by trusting a config file to stay accurate.
npx create-nx-workspace@latest myworkspace
nx graphnx graph opens a visual map of every app and library and how they depend on each other. That graph is the thing everything else in Nx is built on top of.
This is the piece that made the workspace worth it for me. Instead of running the full test suite on every project every time, Nx compares your branch against a base commit and figures out which projects were actually touched, directly or through a dependency.
nx affected --target=test
nx affected --target=build
nx affected:graphChange a shared utility library and only the apps that import it get rebuilt and retested. Change a leaf app nothing else depends on, and that's the only thing that runs. On a repo with a dozen projects, that difference is minutes versus seconds.
Nx hashes the inputs to a task (source files, config, dependency versions) and stores the output. Run the same task again with the same inputs and it skips execution entirely and replays the cached result.
{
"targetDefaults": {
"build": {
"cache": true,
"inputs": ["default", "^default"]
}
}
}Locally this saves you from rebuilding things you already built. Wired up to Nx Cloud, the cache is shared across the team and CI, so if a teammate already built a given commit, your machine or your pipeline just downloads the result instead of redoing the work.
build for one project usually needs its dependencies built first. dependsOn: ["^build"] in nx.json tells Nx that before running a task on a project, run that same task on everything it depends on.
{
"targetDefaults": {
"build": {
"dependsOn": ["^build"]
}
}
}The ^ means "on this project's dependencies," not on the project itself. Nx reads the graph, works out the correct order, and runs independent branches of it in parallel instead of one long serial chain.
Scaffolding a new library by hand meant copying an existing one and manually renaming everything, which is exactly the kind of thing I forget a file in. Generators do it consistently:
nx generate @nx/react:library shared ui
nx generate @nx/node:application api gatewayEvery plugin (@nx/react, @nx/node, @nx/next, and so on) ships generators for its own project type, so a new library follows the same shape as every other one in the workspace without me deciding those conventions fresh each time.
| Concept | What it does |
|---|---|
nx graph | Visual map of every project and its dependencies |
nx affected --target=X | Runs a target only on projects actually touched by your changes |
| Local cache | Skips a task entirely if its inputs haven't changed |
| Nx Cloud | Shares that cache across the team and CI |
dependsOn: ["^build"] | Builds a project's dependencies before the project itself |
nx generate | Scaffolds a new app or library using a plugin's conventions |
Still early with it, but the mental model finally makes sense: Nx isn't really about jamming everything into one folder. It's a dependency graph plus a cache, and every feature (affected, caching, task pipelines) is just a different way of using that graph so you stop rebuilding and retesting things that never changed.