Tooling
Staying fast with TypeScript and Nx in a monorepo
A monorepo is a bet that sharing code is worth the coordination cost. Nx and TypeScript are what make that bet pay off instead of turning into a slow, tangled build where every change risks everything and nobody remembers why the CI takes forty minutes.
I work in an Nx workspace daily — multiple apps, a stack of shared libraries, strict TypeScript throughout — and the difference between a monorepo that helps and one that hurts comes down to a handful of habits. None of them are exotic. All of them are easy to skip until it's expensive to add them back.
Strict types are the cheapest tests you'll ever write
Strict TypeScript — actual strict mode, not 'strict except the flags that were annoying' — catches a whole category of bugs before the app ever runs, and it does it in the editor while you type. On a shared codebase, that fast feedback is worth more than any amount of documentation, because it's documentation that can't go stale.
The compound effect shows up at refactor time. Renaming a field on a type that four apps consume is terrifying in JavaScript and boring in strict TypeScript: the compiler hands you a checklist of every place that needs to change. Boring refactors are the entire point — a codebase you're afraid to change is a codebase that's already dying, whatever the commit graph says.
Draw the library boundaries like they're load-bearing
Nx's real power isn't the generators or the cache — it's the project graph. But the graph is only as good as your boundaries. The pattern that works: small, single-purpose libraries (ui, data-access, feature, util) with enforced dependency rules, so a `ui` library physically cannot import from a `feature` library.
Those module-boundary lint rules feel bureaucratic on day one and priceless on day two hundred. Without them, every library slowly grows tendrils into every other, and one day you discover that changing a date-formatting helper invalidates the cache for the entire workspace. The rules aren't about purity; they're about keeping 'affected' small.
Let the machine skip the work
The Nx mental model is simple: if the inputs didn't change, don't redo the work. Local computation caching means the second run of anything is instant; `nx affected` means CI only builds and tests what a given PR actually touches. On a healthy workspace, a one-line change to one app should trigger a build measured in seconds, not the full-workspace penalty.
When the cache stops helping, it's almost always a boundaries problem in disguise — some shared library that everything depends on is changing too often, so everything is always 'affected'. The fix is architectural, not configurational: split the hot library so its volatile parts stop invalidating its stable ones.
Keep the developer loop honest
Tooling only helps if the loop stays tight. The habits that keep an Nx workspace pleasant: run typecheck and lint on affected projects before pushing, keep generator templates current so new libraries start consistent, upgrade Nx on schedule rather than letting versions rot, and treat a slow CI run as a bug with an owner rather than weather.
The metric I actually watch is time-to-confidence: how long between saving a file and knowing the change is safe. Strict types give the first answer in milliseconds, cached tests the second in seconds, affected builds the third in minutes. Keep those three honest and a monorepo with a dozen apps can feel lighter than a single app with none of it.
The payoff
The end state is worth the setup: one PR can update a shared component and every consumer of it, typechecked end to end, tested only where it matters, reviewed in one place. Cross-cutting changes stop being projects and become Tuesdays.
That's the real argument for the monorepo bet — not code sharing in the abstract, but the speed with which a small team can move across a large surface without breaking it. TypeScript makes the moves safe; Nx makes them fast. You need both.
