Skip to content

Craft graph vs Nx

Nx organises the workspace. The Craft graph judges the shape of one app. They are complementary — comparing depConstraints to assertHttpEndpointUnique is comparing a city plan to a wiring diagram.

Use Nx when the constraint is about projects, TypeScript imports, or what CI should rerun. Use Craft when the constraint is about who may yield whom, who owns an HTTP endpoint, or whether a route proof stayed armed. Not instead of architecture rules — this page is the why; that page is the how.

They already run together

The demo suite is an Nx target: npx nx architecture demo. Craft does not replace Nx; it adds a graph Nx cannot see.

Two graphs, two altitudes

NxCraft
Nodean app or a liba route, a service, a GET users, a craftUnique identity
Edgea TypeScript import (static / dynamic / implicit)depends-on, provides, calls, writes, checks, …
Questionwho may import whom, and what should rerun?who may yield whom, and who owns this endpoint?
EnforcementESLint @nx/enforce-module-boundariesVitest on the graph (assert*, noExclusiveLink)
Visualisationnx graph — projects and tasks, --affectednpx craft-graph --format html — a route expanding into services and HTTP

Nx orchestrates. Craft asserts.

What Nx cannot see

Nx's node is a project. Everything inside apps/shop/src is opaque: routes, providers, yield*, HTTP, storage. That is not a bug — the project graph is built to be cheap enough to drive CI. The holes appear as soon as you want to judge an app, not a workspace.

Nx seesNx missesCraft counterpart
demo imports @craft-ng/coreyield* CheckoutApi in the same appdepends-on
Tags on a lib (scope:admin)Two features that share one appassertPathBoundaries, noExclusiveLink
A circular import between libsA service cycle A → B → A inside one tsconfigassertNoDependencyCycles
That HttpClient was importedThat GET users is called from two APIsassertHttpEndpointUnique
Nothing about storage keysTwo queries sharing a craftUnique identityassertCraftUnique
Nothing about unused type aliasesA commented-out CanRun that still compilesassertRouteDiProofs
Nothing about computedA craftComputed that calls a method or writes a source$assertCraftComputedPure

Three consequences follow.

An edge is an import, not DI. yield* CheckoutApi does not cross a project boundary. Module-boundary ESLint never fires. The Craft graph records it as depends-on.

Isolation costs a library. For depConstraints to protect a feature, that feature must be its own project — barrel, tags, often a build. Craft states the same intention on folders and on yield, without extracting forty libs.

ESLint judges a file. @nx/enforce-module-boundaries sees one import. assertHttpEndpointUnique and assertRouteDiProofs see the whole graph, including lazy loadChildren collections a parent proof never covers.

Nx Enterprise Conformance can add workspace rules on the project graph and the file tree. Recreating Craft's AST analysis (DI, yield, HTTP) there would be rewriting @craft-ng/dev-tools.

What Craft cannot see

The Craft graph is one TypeScript program (analyzeDependencyGraph takes one tsconfig). It does not become a build system.

Craft seesCraft missesNx counterpart
Who yields whom inside an appWhich projects CI should rerunproject graph + nx affected
Folder lanes on depends-onA deep import that bypasses a lib's index.tsenforce-module-boundaries
A duplicate GET usersNest imported from a frontend projectbannedExternalImports
A service cycle in apps/shoporderscustomers as libscircular project dependencies
Craft/Angular TypeScriptPython, Nest, assets, configspolyglot project graph, implicit deps
A failing Vitest assertionA generator that rewrites the fileConformance fix generators
Seconds of ts-morph analysisMillisecond cache hits on unchanged libslocal / remote computation cache

The Craft graph asserts. The Nx graph executes: what to build, test, cache, parallelise. Without Nx (or an equivalent), a green architecture suite does not scale in CI.

assertPathBoundaries protects a modular monolith. It does not split the task graph. Changing one feature folder still invalidates the whole app architecture target — analysis reloads the tsconfig, in seconds, not milliseconds.

Craft is a Craft/Angular analyser. Outside that dialect the graph is empty. Nx tags Nest, React, Python, assets and config files.

The overlap

Same intention, different edge.

IntentionNxCraftTrap
This layer must not talk to that onesourceTag: type:uionlyDependOnLibsWithTags: type:utilassertPathBoundariessrc/app/ui/** must not depends-on src/app/data/**Nx requires libs. Craft runs inside one tsconfig, on yield, not on the import.
No cyclesorderscustomersorders (project imports)assertNoDependencyCycles on services / craftComputedA service cycle inside apps/shop is green for Nx and red for Craft.
See the graphnx graph --affected (projects or tasks)craft-graph --format html centred on a routeOne shows who rebuilds. The other shows who injects.

The assertPathBoundaries helper is the closest cousin of depConstraints. Nx tags projects and forbids TypeScript imports. Craft tags folders on the Craft graph and forbids depends-on (optionally calls) — including inside one app, where module-boundary ESLint does not run. Setup and examples: Architecture rules.

How they complement

Do not recode depConstraints as Vitest, and do not recode Craft's AST analysis as an Nx Conformance rule. Each tool stays on its graph.

Keep Nx forKeep Craft for
Monorepo layout, tags between libs, public API barrelsHTTP ownership, craftUnique identities
nx affected, local / remote cache, task graphRoute DI proofs staying armed
Banning an npm package by tagPure craftComputed, service-level cycles
Generators, plugins, polyglot projectsnoExclusiveLink, browser-boundary HTTP

Folder lanes are not affected CI

Extracting Nx libraries gives you nx affected. It still does not prove a single service owns GET users. assertPathBoundaries keeps features apart inside one app. It still does not slice the task graph. Both layers stay necessary.

The working reference is the demo app: an Nx architecture target that runs Vitest on the Craft graph. Copy that layout from Architecture rules.

See Also