bitarch.dev
Engineering

NX Monorepos: One Repo, CI That Keeps Up

One repo. Any number of apps, packages, and modules. Here is how to set it up so it stays manageable as it grows.

Dhruba Baishya
Dhruba Baishya
Software Engineer
Mar 17, 2026
7 min read

I have run monorepos where CI crawled because there was a workflow defined for every module. I have also worked in repos where changing a shared package silently broke three apps because no one knew they depended on it. Both problems are solvable. NX solves them by understanding your dependency graph and using that knowledge to do only the work that actually matters.

Here is how I set it up — the folder structure, the tooling, and the CI workflow that replaced a folder full of GitHub Actions files.

Folder Structure

Two top-level directories. That is it.

apps/
packages/

apps/

Everything deployable. Web apps, mobile apps, backends. Each is an independent unit you can build and ship on its own.

packages/

Shared libraries. UI components, SDKs, config packages. Nothing here gets deployed — it only exists to be consumed by apps. That includes Python packages under packages/python/, not just JS/TS.

Every new directory you add has a clear home. In practice, that means fewer conversations about where a new package or app should live.

Dependency Inference

NX reads your import statements and builds the dependency graph from them. You do not declare dependencies between projects in any config file. You write code, and NX knows that apps/web depends on packages/ui because the import says so.

Manual dependency declarations go stale. Someone adds an import, forgets to update the config, and your tooling lies to you. With inference, the graph is always accurate.

Not everything shows up as a package dependency, though. NX also tracks implicit dependencies — relationships that exist outside of package.json. A shared TypeScript path alias that multiple projects resolve through, a root-level config file that several projects reference, build outputs that one project consumes from another. None of those show up as npm dependencies, but they are real relationships. You can declare them explicitly with implicitDependencies in a project's project.json, and NX will include them in the graph. That means nx affected works correctly even for dependencies that never touch node_modules.

The same inference works for Python. The @nxlv/python plugin reads [tool.uv.sources] entries in pyproject.toml and builds dependency edges from them automatically — you just need inferDependencies: true in nx.json. A FastAPI backend that depends on a shared Python SDK shows up in the graph the same way a Next.js app that imports a shared JS package does.

nx affected

When you run nx affected, NX computes which projects have changed — based on git diff against your base branch — and runs the target only for those projects and anything that depends on them.

If you change packages/ui, NX knows every app that imports it and runs lint, typecheck, and tests across all of them. If you change apps/web and nothing depends on it, only apps/web gets checked.

As the repo grows, CI time does not grow proportionally. You only pay for what changed.

When you change a shared package, NX automatically runs checks on every module that depends on it. You do not have to remember which apps import that package. If your change breaks a downstream consumer, CI catches it before it merges. If it does not break anything, CI finishes fast because only what is affected runs.

nx graph

Run nx graph and NX opens a browser with a live visualization of your full dependency graph. Every project, every edge, rendered clearly.

You can filter it down to a single project and see exactly what it depends on and what depends on it. When something breaks and you need to see what is affected, this is the first tool to reach for.

NX VS Code Extension

The extension brings the project graph into your editor. You get the same visualization as nx graph without opening a browser tab — available as a panel inside VS Code.

From the sidebar, you can run and debug any NX target — lint, test, build, serve — for any project in the monorepo. If you are mid-feature and want to run tests just for the package you are touching, you can do it directly from the sidebar. No terminal, no remembering the project name, no typing out the full nx run command.

It also shows you which projects are affected by your current changes, directly in the editor. You do not have to switch to a terminal or browser to know what a change touches.

One GitHub Actions Workflow

You do not need a workflow file per app or per module type. One file handles everything.

# .github/workflows/ci.yml
- run: npx nx affected -t lint typecheck test --base=origin/main

That single command covers every module in the repo. NX handles the scoping — GitHub Actions just provides the runner. That includes Python projects: the same nx affected run lints and type-checks FastAPI backends alongside the Next.js apps. No separate Python CI config needed.

When you add a new app, it automatically falls under the same CI coverage. No new workflow file, no new job definition. NX already has the new project in its graph, so the affected computation picks it up on the next run.

Pre-commit Hooks

Pre-commit hooks are only useful if they are fast. Running the full repo on every commit kills developer velocity.

With nx affected scoped to your staged changes, pre-commit hooks run lint and typecheck only for what you have actually touched. On a large repo, the difference between checking 30 modules and checking 1 or 2 is the difference between a hook that takes seconds and one that takes minutes.

Fast hooks get respected. Slow hooks get skipped.

Most of the complexity I used to manage manually — which apps to test, which CI jobs to run, which hooks to skip — is just gone. NX handles it from the graph. There are plenty of other things NX does well, but these are the ones that have made the biggest difference in keeping the repo fast and the team from stepping on each other.

Read More from the Author

The Technical Debt of AI Speed

AI helps us ship faster than ever, but it's also creating a new kind of technical debt. Here is how to manage the speed without losing quality.