{"slug": "archunitts-is-not-another-eslint", "title": "ArchUnitTS is not another ESLint", "summary": "ArchUnitTS, a TypeScript port of the Java architecture testing library ArchUnit, lets developers enforce architecture rules as test code. Developer Joon Quixote explains that while the tool is valuable for graph-dependent rules like cycle detection, simple dependency bans may be better handled by ESLint or project structure. The post also addresses the challenge of rules living only in documentation, especially as AI agents generate more code.", "body_md": "*Originally published at https://blog.joonquixote.com/en/posts/not-another-eslint/.*\n\nArchUnitTS is built on architecture tests, and it does what it promises: it takes the architecture rules that erode most easily and lets you enforce them in test code.\n\nBut the feedback only lands when the suite runs, which is reason enough to look again at how you are actually using it.\n\nThis post is about where that line falls: which rules to keep in ArchUnitTS, and which to hand to ESLint or to the structure of the project.\n\nIt also covers what to do about rules that live only in a markdown file, now that AI agents write a growing share of the code.\n\nMost teams have at least one architecture rule they have agreed on.\n\nSomething like \"the presentation layer does not depend on the database layer.\"\n\nThe trouble is that rules like this are usually held up by nothing but a document and code review.\n\nThe bigger the project gets and the more people touch it, the more the rule drifts, and the harder that drift is to undo.\n\nI have watched it happen. A project I worked on had a perfectly clear layering rule and still ended up with a circular dependency, assembled one missed import at a time in review.\n\nA cycle is just two files importing each other, either directly or through a chain of other files, and once one exists it is genuinely hard to work out where to cut.\n\nThat is what pushed me toward enforcing rules in code rather than trusting memory and review to catch them.\n\nThe tool I settled on was [ArchUnitTS](https://github.com/LukasNiessen/ArchUnitTS).\n\nIt lets you write architecture rules as test code.\n\nThe rules are ordinary test cases, so they need no runner of their own.\n\nThey run with everything else: when you run `npm test`\n\nlocally, in a pre-commit hook, and in CI.\n\nA rule that used to live in a document becomes part of the test suite, which is to say it becomes code that runs.\n\nHere is a test that checks for cycles and for a layering rule, written for Jest.\n\n``` js\nimport { projectFiles } from 'archunit';\n\nit('has no cycles inside src', async () => {\n  const rule = projectFiles().inFolder('src/**').should().haveNoCycles();\n\n  await expect(rule).toPassAsync();\n});\n\nit('keeps the presentation layer off the database layer', async () => {\n  const rule = projectFiles()\n    .inFolder('src/presentation/**')\n    .shouldNot()\n    .dependOnFiles()\n    .inFolder('src/database/**');\n\n  await expect(rule).toPassAsync();\n});\n```\n\n*Written as a test, a violation surfaces as a failing test. The rule is held by verification rather than by agreement.*\n\nArchUnit is the Java library for checking architecture rules with unit tests.\n\nIt lets you write rules like \"this package must not depend on that one\" as JUnit tests.\n\nArchUnitTS carries the idea over to TypeScript, and is not officially affiliated with ArchUnit.\n\nWhen I introduced ArchUnitTS to the team, that was the first question back.\n\n\"Can't we just do that with ESLint?\"\n\nI said no at the time. Turning it over afterwards, it was half right.\n\nThe two tools part company at the moment they tell you about a violation.\n\nTo check relationships between files, the way cycle and layering rules do, ArchUnitTS parses the AST of the whole project and builds a dependency graph out of it.\n\nA lint rule can decide from the import statements in one file; these rules cannot be answered without the whole graph.\n\nThat is exactly what lets it express rules lint cannot, and it is also why its feedback lands later than an editor running ESLint on save.\n\nThat gap is what made me question whether simple dependency rules belong in ArchUnitTS at all.\n\nLooking back over the rules we had really written with ArchUnitTS, nearly all of them were some variation of \"folder A must not depend on folder B.\"\n\nFor the rules that genuinely need the whole graph, like cycle detection and code metrics, it earned its keep. For plain dependency bans it started to feel like over-engineering.\n\nEmpty test protection was the part I valued most.\n\nIt fails a rule that ends up checking zero files.\n\nA typo in a folder path would otherwise leave the rule matching nothing and passing in silence; ArchUnitTS turns that into a failure instead, which is what stops you believing in a rule that is not running.\n\nIt does not cover every rule, though.\n\nChecking it myself, cycle detection is the exception: hand `haveNoCycles`\n\na folder with a typo in it and the rule passes quietly, having examined nothing.\n\nThe library documents this - cycle checks test the unfiltered set of files for emptiness rather than the filtered set they actually analyse.\n\nCycle detection is the single strongest reason I had for keeping ArchUnitTS at all.\n\nThe protection is missing exactly where I lean on it hardest, which makes it the one exception worth remembering.\n\nEven so, I was not convinced a simple dependency ban is best checked by a test.\n\nThere was one more question worth putting to myself.\n\n\"Do I really need architecture rules complicated enough to justify this?\"\n\nSometimes the answer is yes. But when it is, I have also wondered whether the real fix is to simplify the structure rather than add another rule.\n\nMove a simple dependency ban into ESLint and the feedback shows up in the editor.\n\n`eslint-plugin-import`\n\nhas `no-restricted-paths`\n\n, which expresses a layering rule in a few lines of config.\n\n``` python\n// eslint.config.js\nimport importPlugin from 'eslint-plugin-import';\n\nexport default [\n  {\n    plugins: { import: importPlugin },\n    rules: {\n      'import/no-restricted-paths': [\n        'error',\n        {\n          zones: [\n            { target: './src/presentation', from: './src/database' },\n            { target: './src/business', from: './src/database' },\n          ],\n        },\n      ],\n    },\n  },\n];\n```\n\nWith that in place, importing the database layer from presentation puts an error on the screen as you type it.\n\nThe moment the violation is created and the moment it is found become the same moment.\n\nIn a monorepo you can go a step further and put the rule in the structure itself rather than in configuration.\n\nSplit the layers into packages, and let each package.json declare only the dependencies it is allowed.\n\n```\n{\n  \"name\": \"@app/presentation\",\n  \"dependencies\": {\n    \"@app/business\": \"workspace:*\"\n  }\n}\n```\n\n`@app/presentation`\n\nhas no `@app/database`\n\nin its dependencies, so importing the database layer from presentation fails at module resolution.\n\nThere is no rule left to check, because the violation is not expressible.\n\nBefore adding a new library, it is worth asking whether you can pull the feedback earlier or push the rule into the structure.\n\nPeople are no longer the only ones who can break a rule.\n\nAI coding agents write a fast-growing share of the code.\n\nSo teams have started keeping markdown files, CLAUDE.md or AGENTS.md, to hand the project's rules and context to the agent.\n\nYou write down \"the presentation layer does not depend on the database layer\" and hope the agent honours it.\n\nBut what that file really is, is a blob of context.\n\nAn agent consults context; it does not promise to follow it.\n\nA rule that human review already failed to hold is not going to be held by a document you cannot even confirm was read.\n\nSo a rule you put in a document needs an executable check alongside it.\n\nWhen the agent breaks the rule, a lint error or a failing test is an unambiguous signal, and that signal feeds straight back into the loop where the agent fixes its own code.\n\nThe distinction drawn above, about when a rule gets checked, applies to agents exactly as it does to people.\n\nA simple dependency ban is caught by lint inside the agent's own loop, and a package boundary makes the violation impossible to begin with.\n\nWhat changes is where ArchUnitTS sits.\n\nAmong the rules people write out in prose in a markdown file are sentences like \"there are no cycles anywhere in src\" and \"the overall structure follows this diagram.\"\n\nLint, which only ever sees one file's imports, cannot check those; ArchUnitTS's cycle detection and its diagram-conformance check map onto them almost one to one.\n\nThat makes a division of labour possible, where the document keeps the intent and the test carries the verification.\n\nArchUnitTS is genuinely useful where a lint rule cannot reach: cycle detection, code metrics such as cohesion, which measures how tightly a class's methods and fields actually hang together, and checking real code against a UML diagram.\n\nUse it only for simple dependency bans, though, and it turns into another ESLint, and a slower one at that.\n\nSo when someone asks whether ESLint can just do that, this is my answer now.\n\nFor a simple dependency rule, yes, it can.\n\nFor a rule about the structure as a whole, no, it cannot.\n\nThree questions make the choice easier before you add a rule.\n\nCan this only be checked by running the tests, can it be checked the moment the file is saved, or can the structure rule it out entirely?\n\nIn the age of agents there is a fourth.\n\nDoes this rule exist only in a document, or does it come with a check that runs?\n\nIn the end, what matters is the attitude rather than the tool.\n\nRather than settling on one library as the answer, keep suspecting there is something that fits the situation better, and go looking for it.\n\nDoubting a tool I had adopted as a matter of course is how I learned a sturdier way to hold an architecture together.", "url": "https://wpnews.pro/news/archunitts-is-not-another-eslint", "canonical_source": "https://dev.to/hsskey/archunitts-is-not-another-eslint-2eh8", "published_at": "2026-08-20 00:00:34+00:00", "updated_at": "2026-08-20 00:14:14.612373+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["ArchUnitTS", "ArchUnit", "Jest", "ESLint", "Joon Quixote"], "alternates": {"html": "https://wpnews.pro/news/archunitts-is-not-another-eslint", "markdown": "https://wpnews.pro/news/archunitts-is-not-another-eslint.md", "text": "https://wpnews.pro/news/archunitts-is-not-another-eslint.txt", "jsonld": "https://wpnews.pro/news/archunitts-is-not-another-eslint.jsonld"}}