My 11 Favorite ESLint Rules

5 min read Original article ↗

Handy rules to adhere to keep adhering to good practices for developers and AI tools

Ali Kamalizade

Press enter or click to view image in full size

Photo by Mark Duffel on Unsplash

At Sunhat, we spend a lot of time in TypeScript. Between technologies like Angular, NestJS, and Nx, there’s a lot of code moving fast across a SaaS product. The bigger a codebase grows, the more you rely on the small invisible things that keep it maintainable. ESLint is one of those things.

I’ve come to see ESLint not just as a linter but as a guardrail system. It keeps you from slowly drifting into chaos. We have a custom ESLint setup that evolved over time, and there are a few rules that have proven helpful to how we work.

This not only applies to the engineers themselves but also to AI tools like Cursor. We use tools like Cursor increasingly for refactorings, quick iterations, and exploring changes. When your codebase has strict, automated constraints, the AI doesn’t just write code: it writes "your" code. It is less hallucinating patterns that don’t exist and starts following your architecture and good practices.

ESLint rules effectively teach them how your code is supposed to look. If not already done automatically, you can let your AI tools make use of ESLint to validate itself. Hence the AI has far fewer degrees of freedom when it’s generating code. That’s a good thing.

These are the categories of rules we’ll be looking at:

  • Avoiding to delete temporary code
  • Prefer
  • Avoid common bugs
  • Consistency

Without further ado, let’s have a look.

1. @nrwl/nx/enforce-module-boundaries

If you use Nx, this rule is non-negotiable. We treat our Nx workspaces as modular systems. Each app and library has a clear domain. Without boundaries, developers will eventually take shortcuts — importing code from where it’s convenient rather than where it belongs.

This rule prevents that. It enforces dependency constraints between libraries, ensures each domain stays self-contained, and stops circular imports before they ever happen. Every time it blocks a “quick import” across layers, it saves you from future architectural debt.

Here is an example:

'@nx/enforce-module-boundaries': [
'error',
{
allow: [],
depConstraints: [{
sourceTag: 'type:app',
onlyDependOnLibsWithTags: ['type:feature', 'type:shared']
},
{
sourceTag: 'type:feature',
onlyDependOnLibsWithTags: ['type:shared', 'type:feature']
},
{
sourceTag: 'type:shared',
onlyDependOnLibsWithTags: ['type:shared']
}
]
}
]

2. @typescript-eslint/member-ordering

Codebases aren’t just about what they do, but how they feel when you read them. This rule enforces consistent ordering of class members: constructors, lifecycle hooks, public methods, private helpers, and so on. You don’t have to wonder where to find things.

'@typescript-eslint/member-ordering': ['warn', {
default: ['field', 'get', 'constructor', 'method', 'signature']
}]

3. @typescript-eslint/naming-convention

Naming consistency is one of those things you don’t appreciate until it’s gone.

This rule enforces our conventions for classes, interfaces, constants, and private members. It’s the kind of detail that prevents friction: like whether we prefix private fields with _ or not, or if interfaces should start with I. More importantly, it makes code reviews shorter because we no longer have to discuss naming at all.

 '@typescript-eslint/naming-convention': ['error', {
selector: 'default',
format: ['camelCase']
}, {
selector: ['class', 'interface', 'typeParameter', 'typeAlias'],
format: ['PascalCase']
}, {
selector: ['objectLiteralProperty', 'objectLiteralMethod'],
format: null
}, {
selector: ['enum', 'enumMember'],
format: ['PascalCase', 'snake_case']
}, {
selector: 'parameter',
format: ['camelCase'],
leadingUnderscore: 'allow'
}, {
selector: 'variable',
format: ['UPPER_CASE', 'camelCase'],
leadingUnderscore: 'allow'
}, {
selector: 'typeProperty',
format: null,
filter: {
regex: '^_count$',
match: true
}
}]

4. @typescript-eslint/prefer-optional-chain

Instead of deeply nested null checks, you just write foo?.bar?.baz. It reads like a sentence and eliminates a ton of boilerplate.

We enforce it to keep code clean and expressive — and to avoid the old “cannot read property of undefined” errors that everyone loves.

5. @typescript-eslint/prefer-nullish-coalescing

This rule complements the previous one. It enforces ?? instead of || when dealing with default values.

Why? Because 0 and " are valid values. With ||, they’re treated as falsy, and you might accidentally override them. ?? fixes that by only falling back on null or undefined. Small difference that can prevent easy-to-miss-bugs.

6. @typescript-eslint/switch-exhaustiveness-check

If you’re switching over a union type, this rule ensures all possible cases are handled. If a new variant is added later and you forget to handle it, then ESLint reminds you.

For us, this is crucial in large enums and discriminated unions that evolve as our business logic grows.

7. etc/no-commented-out-code

Dead code is noise. Commented-out code is lying noise: it suggests something might matter when it doesn’t. We don’t leave commented code behind. If it’s useful, it belongs in Git history. If it’s not, delete it.

This rule keeps our diffs smaller and our minds clearer.

8. lodash-fp/use-fp

We use lodash/fp instead of the classic lodash API. The functional version promotes immutability and better composability.

This rule enforces that all lodash imports come from lodash/fp. It prevents accidental mutable operations that could mess with state.

9. no-console

Logging is great. But stray console.logs in production code aren’t.
We use structured logging through proper monitoring and telemetry systems (Sentry, platform logs, etc.).

This rule blocks console.log and similar methods from creeping into production code while allowing explicit exceptions where necessary.

10. no-param-reassign

Mutating function parameters is one of those small bad habits that quietly leads to bugs.

We want our functions to be predictable and side-effect free.
This rule forces you to create new variables instead of mutating inputs. It makes data flow more explicit and debugging much easier.

11. prefer-const

If a variable never changes, it should be const. This rule is simple, but it’s one of those that make JavaScript feel more like a safe, typed language.

const means: “this won’t change.” It signals intent and thereby reduces mental load.

Conclusion

Our ESLint config is a small example of our philosophy in action. It encodes what we care about: safety, clarity, and consistency. And when those things are built into your tooling, your team gets to focus on what really matters: building. Of course it’s up to your team to decide which rules make sense. And if existing rules don’t cover it: why not create your own?

Thanks for reading this post. Let me know in the comments about useful tools and rules you are using.