How to Build a Custom ESLint Plugin for Clean Code in a TypeScript Monorepo

User avatar placeholder
Written by Tamzid Ahmed

September 1, 2026

Modern teams rely on ESLint to keep codebases readable and maintainable, but generic rules often miss project‑specific conventions. In a TypeScript monorepo, sharing a tailored lint plugin ensures every package follows the same clean‑code standards without duplicating configuration. This guide walks you through creating, testing, and publishing a custom ESLint plugin that enforces your team’s clean‑code rules.

Why Create a Custom ESLint Plugin for a TypeScript Monorepo?

When multiple packages share a repository, inconsistencies can creep in—different naming conventions, missing JSDoc, or unsafe type assertions. A custom plugin lets you:

  • Enforce project‑specific clean code practices.
  • Provide instant feedback during development.
  • Avoid repeating .eslintrc.js overrides in each package.

By centralising these rules, you reduce review friction and improve overall code quality.

Understanding ESLint Plugin Architecture

An ESLint plugin is an npm package that exports:

  • rules: objects defining the logic and metadata for each lint rule.
  • processors (optional): for handling non‑JS file types.
  • configs (optional): shareable configurations that bundle rules and presets.

Each rule consists of a create function that returns visitor functions for the AST nodes you want to inspect. The popular @typescript-eslint/parser lets you work with TypeScript‑aware ASTs.

Rule Metadata Essentials

Every rule should include:

  1. meta.docs.description – a clear, snippet‑friendly explanation.
  2. meta.fixable – set to 'code' if the rule can auto‑fix.
  3. meta.schema – JSON‑schema for rule options, enabling configurability.

Keeping metadata accurate helps IDEs surface useful explanations and powers automatic fix‑application.

Setting Up the Plugin Project

Follow these steps to scaffold a reusable ESLint plugin for your monorepo:

  1. Create a new package: mkdir packages/eslint-plugin-clean-code && cd $_
  2. Initialize: npm init -y (set name to @your-scope/eslint-plugin-clean-code).
  3. Install peer dependencies: npm i -D eslint @typescript-eslint/parser @typescript-eslint/typescript-estree
  4. Add "main": "index.js" and "peerDependencies" to package.json.
  5. Create lib/rules/ directory for rule implementations.
  6. Export rules in index.js:
// index.js
const noMagicNumbers = require('./lib/rules/no-magic-numbers');
module.exports = {
  rules: {
    'no-magic-numbers': noMagicNumbers
  },
};

This structure keeps the plugin lightweight and easy to extend.

Writing Your First Clean Code Rule

Let’s build a rule that bans numeric literals outside of named constants—a common clean‑code guideline.

Rule Logic

The rule will:

  1. Visit Literal nodes with a numeric value.
  2. Ignore literals that appear as enum members, const declarations, or inside TSTypeLiteral.
  3. Report a warning with a suggested fix to replace the literal with a constant reference.

Implementation

// lib/rules/no-magic-numbers.js
module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'Disallow numeric literals that are not named constants',
      recommended: true,
    },
    fixable: 'code',
    schema: []
  },
  create(context) {
    return {
      Literal(node) {
        if (typeof node.value !== 'number') return;
        const parent = context.getAncestors().pop();
        // Skip if the literal is inside a const declaration or enum
        if (parent && (parent.type === 'VariableDeclarator' && parent.init === node) ||
            parent.type === 'TSEnumMember') {
          return;
        }
        context.report({
          node,
          message: 'Replace magic number {{value}} with a named constant.',
          data: { value: node.value },
          fix(fixer) {
            // Suggest creating a constant – in practice you’d need a more sophisticated fix
            return null;
          }
        });
      }
    };
  }
};

Publish the package (npm publish --access public) and make it available to your monorepo.

Integrating the Plugin in a Monorepo

Assuming you use a tool like Nx or TurboRepo, add the plugin to the root package.json devDependencies and reference it in a shared ESLint configuration:

// .eslintrc.base.js
module.exports = {
  plugins: ['@your-scope/clean-code'],
  extends: [
    'plugin:@your-scope/clean-code/recommended'
  ],
  rules: {
    '@your-scope/clean-code/no-magic-numbers': 'error'
  }
};

Each app or library can then extend .eslintrc.base.js, guaranteeing consistent enforcement across the monorepo.

Best Practices and Tradeoffs

When authoring custom rules, consider:

  • Performance: Avoid heavy computations in rule listeners; prefer simple AST checks.
  • Configurability: Expose options via meta.schema so teams can toggle strictness.
  • Documentation: Include a README with examples of correct and incorrect code.
  • Versioning: Follow semantic versioning; breaking changes to rule logic warrant a major bump.

Balancing rule precision with false positives is key—too aggressive a rule slows down development, while too lenient defeats the purpose.

Conclusion

Creating a custom ESLint plugin for a TypeScript monorepo empowers teams to enforce clean‑code standards centrally, reduce review overhead, and maintain consistency across packages. By following the steps outlined—setting up the plugin project, writing focused rules, and integrating via a shared config—you’ll gain immediate, measurable improvements in code quality. Start small, iterate based on team feedback, and publish your plugin to unlock scalable linting across your entire codebase.

Leave a Comment