Custom ESLint rules allow you to enforce team-specific conventions, project architectural patterns, and codebase-specific best practices that aren't covered by existing ESLint plugins. Nx provides two main approaches for creating and using custom ESLint rules in your workspace.
Understanding the Two Approaches
Section titled “Understanding the Two Approaches”There are two main approaches for custom ESLint rules in Nx workspaces:
Package Manager Workspaces (npm/yarn/pnpm/bun): Create a dedicated ESLint plugin package (e.g.,
nx g @nx/js:lib packages/eslint-rules) that's symlinked via your package manager. Import rules as you would any npm package.loadWorkspaceRulesUtility: Use the@nx/eslint-pluginutility to load rules from any directory. This works whether or not you use package manager workspaces.
Choose the approach that fits your workspace setup:
| Scenario | Recommended Approach |
|---|---|
| Using npm/yarn/pnpm/bun workspaces | Package Manager Workspaces |
| Not using package manager workspaces | loadWorkspaceRules |
| Want rules as a publishable package | Package Manager Workspaces |
| TypeScript rules with minimal config | loadWorkspaceRules |
Approach 1: Package Manager Workspaces
Section titled “Approach 1: Package Manager Workspaces”If your workspace uses npm, yarn, pnpm, or bun workspaces, you can create custom ESLint rules as a regular package. This approach treats your custom rules like any other internal dependency.
Step 1: Create the ESLint Plugin Package
Section titled “Step 1: Create the ESLint Plugin Package”Create a new library for your ESLint plugin:
nx g @nx/js:lib packages/eslint-rulesStep 2: Structure the Plugin
Section titled “Step 2: Structure the Plugin”Your plugin package should export rules following the ESLint plugin format:
import { noFooConst, RULE_NAME as noFooConstName } from './rules/no-foo-const';
export default { rules: { [noFooConstName]: noFooConst, },};Step 3: Create a Rule
Section titled “Step 3: Create a Rule”Each rule should follow the ESLint rule structure. Using @typescript-eslint/utils provides excellent TypeScript support:
import { ESLintUtils } from '@typescript-eslint/utils';
export const RULE_NAME = 'no-foo-const';
export const noFooConst = ESLintUtils.RuleCreator(() => __filename)({ name: RULE_NAME, meta: { type: 'problem', docs: { description: 'Disallow variables named "foo"', }, schema: [], messages: { noFoo: 'Variables named "foo" are not allowed.', }, }, defaultOptions: [], create(context) { return { VariableDeclarator(node) { if (node.id.type === 'Identifier' && node.id.name === 'foo') { context.report({ node: node.id, messageId: 'noFoo', }); } }, }; },});Step 4: Install and Use the Plugin
Section titled “Step 4: Install and Use the Plugin”After creating your plugin package, install dependencies to ensure it's symlinked:
npm install# or: yarn install# or: pnpm install# or: bun installThen use the plugin in your ESLint configuration:
import eslintRules from '@acme/eslint-rules';
export default [ { plugins: { '@acme/eslint-rules': eslintRules, }, rules: { '@acme/eslint-rules/no-foo-const': 'error', }, },];{ "plugins": ["@acme/eslint-rules"], "rules": { "@acme/eslint-rules/no-foo-const": "error" }}Running TypeScript Rules
Section titled “Running TypeScript Rules”When using TypeScript for your ESLint rules, the TypeScript code must be transpiled or interpreted before ESLint can use it. There are several options:
Node.js 22.6+ supports TypeScript natively through type stripping. As of Node 22.18.0 and Node 24, this is enabled by default.
For older versions in the 22.x series, enable it with:
NODE_OPTIONS="--experimental-strip-types" nx lint myprojectAlternatively, if you are on Node 20 or do not want to use Node's strip-types feature, the tsx package provides fast TypeScript execution with ESM support:
npm install -D tsxThen you can register tsx in your eslint.config.mjs file prior to importing the custom rules.
import { register } from 'tsx/esm/api';
const unregister = register();
const eslintRules = await import('@acme/eslint-rules');
export default [ { plugins: { '@acme/eslint-rules': eslintRules, }, rules: { '@acme/eslint-rules/no-foo-const': 'error', }, },];
// cleanupunregister();See the ESM Register API docs for tsx for more information.
Approach 2: Using loadWorkspaceRules
Section titled “Approach 2: Using loadWorkspaceRules”The loadWorkspaceRules utility from @nx/eslint-plugin lets you load ESLint rules from any directory in your workspace. This is particularly useful when:
- You're not using package manager workspaces
- You want rules in a non-standard location
- You need automatic TypeScript transpilation
Basic Usage
Section titled “Basic Usage”import baseConfig from './eslint.base.config.mjs';import { loadWorkspaceRules } from '@nx/eslint-plugin';
// Load rules from a directory relative to workspace rootconst customRules = await loadWorkspaceRules('tools/my-eslint-rules');
export default [ ...baseConfig, { files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], plugins: { custom: { rules: customRules }, }, rules: { 'custom/my-custom-rule': 'error', }, },];How loadWorkspaceRules Works
Section titled “How loadWorkspaceRules Works”The utility:
- Accepts a directory path (relative to workspace root or absolute)
- Looks for an
index.ts,index.mts,index.cts,index.js,index.mjs, orindex.cjsfile - Automatically finds and uses a
tsconfig.jsonfor TypeScript transpilation - Returns the exported rules object
Specifying a Custom tsconfig
Section titled “Specifying a Custom tsconfig”You can provide a specific tsconfig.json path:
const customRules = await loadWorkspaceRules( 'tools/my-eslint-rules', 'tools/my-eslint-rules/tsconfig.lib.json');If not provided, loadWorkspaceRules searches for tsconfig.json starting from the rules directory and traversing up to the workspace root.
Example: Project-Specific Rules
Section titled “Example: Project-Specific Rules”You can load rules from within a project:
import baseConfig from '../../eslint.base.config.mjs';import { loadWorkspaceRules } from '@nx/eslint-plugin';
// Load rules specific to this projectconst projectRules = await loadWorkspaceRules('apps/my-app/eslint-rules');
export default [ ...baseConfig, { files: ['**/*.ts'], plugins: { project: { rules: projectRules }, }, rules: { 'project/component-naming': 'error', }, },];