{
  "$type": "site.standard.document",
  "canonicalUrl": "https://johnnyreilly.com/posts/only-node-subpaths-with-no-restricted-imports-and-perfectionist-sort-imports",
  "description": "This post will show you how to migrate to using Node.js subpaths and how to use the no-restricted-imports and perfectionist/sort-imports ESLint rule to help you sort your imports.",
  "path": "/posts/only-node-subpaths-with-no-restricted-imports-and-perfectionist-sort-imports",
  "publishedAt": "2026-03-15T00:00:00.000Z",
  "site": "at://did:plc:yy3apqjlms24kso7ahn7lbmb/site.standard.publication/3mova7c4nho2b",
  "tags": [
    "javascript",
    "typescript",
    "eslint",
    "node.js"
  ],
  "textContent": "I've recently been experimenting with Node.js subpath imports. My motivation is a general dislike of relative imports. I don't enjoy seeing import { thing } from '../../file.js' style imports in code. By using subpath imports instead I might have import { thing } from 'src/folder/file.js' and that feels much cleaner to me.\n\nBut I also like consistency in my codebase, and I don't want to have a mix of import styles. So while using subpath imports can help me avoid relative imports, I also want to make sure that everyone on the team is using the same style. How? Here's how!\n\n\n\nFirst a warning\n\nBefore I dive into the details, I want to be clear that this is a very opinionated setup. It's not necessarily the best approach for every team or project. I'm sharing it because it's what I'm trying out right now, but I fully expect that there are trade-offs and that it might not be the right choice for everyone. I'm not even sure if it's the right choice for me in the long run. So take this with a grain of salt and consider whether it makes sense for your own context.\n\nNode.js subpaths only\n\nThe first thing to do is to set up Node.js subpath imports in package.json:\n\nThe idea is straightforward:\n\n- When developing / at type-check time, TypeScript maps imports from #src/ to ./src/.\n- At runtime, Node resolves from #src/ to ./lib/ (where our compiled output goes - yours could go somewhere else like dist).\n- Test code maps imports from #test/ to ./test/.\n\nThe practical result is that I can write import { thing } from '#src/features/thing.js'; everywhere and avoid relative import gymnastics.\n\nBut what about Vitest?\n\nTo configure Vitest to understand the subpath imports, I need to add some aliasing to vitest.config.js:\n\nImports in test files will now work as expected, and I can use the same #src/ style imports in tests as well. There's a change required for mocks too, but it's not too bad. A migration in my project took me from this:\n\nTo this:\n\nNot too bad. I actually prefer the import sitting inside the mock factory function, as it makes it clear that the import is only used for the mock setup.\n\nBlock alternatives with no-restricted-imports\n\nAt this point, I have subpaths working, but other import styles are still allowed. Time to end that and enforce that everyone uses the #src/ style for imports.\n\nWe achieve this is two ways using ESLint's no-restricted-imports rule. First, I make sure that no one can use relative imports or other path styles:\n\nThis is intentionally strict. If someone reaches for ~/, @/, or ./, linting pushes them back to #src/....\n\nIs this heavy-handed? Yes. That's the point.\n\nKeep import groups predictable with perfectionist/sort-imports\n\nI'm a big fan of consistent import grouping and sorting, and I achieve that with the perfectionist/sort-imports rule. This rule allows me to define custom groups and enforce a specific order for imports.\n\nOnce everything uses subpaths, I also want imports grouped consistently:\n\nThe key bit is placing subpath imports after value-external imports. It makes #src/ style imports appear after imports from packages. So instead of this:\n\nWe have this:\n\nAs a side note, I have asked on the perfectionist repo about whether an approach like this could / should be supported by default, as opposed to being configurable. I don't know if it will be - but you can follow along here.\n\nDoes this work with TypeScript?\n\nYes! Support has been in place since TypeScript 5.4, thanks to microsoft/TypeScript#55015.\n\nIs this actually a good idea?\n\nPossibly not.\n\nThis approach is highly opinionated, and maybe too rigid for many teams. I'm not fully convinced it's a universally good pattern. I'm trying it because the consistency is appealing, but I haven't come to a settled view yet. I'm writing this post in part to share the approach, but also to help me mull over the idea.\n\nConclusion\n\nIf you want one import path to rule them all, Node.js subpaths plus ESLint enforcement can absolutely do it.\n\nBut this is a strong convention with sharp edges. I don't yet know whether it's a great long-term idea. It is definitely interesting to try out, and I like the consistency it brings. But whether it's the right choice for your team or project is something only you can decide.",
  "title": "Only Node.js subpaths with no-restricted-imports and perfectionist/sort-imports"
}