A complete guide to the SonarLint/SonarJS setup used in this project, and how to replicate it in any NestJS + TypeScript project from scratch.
This project has a dual ESLint config setup:
| Config file | Purpose |
|---|---|
eslint.config.js |
Primary linting — TypeScript rules + SonarJS rules (used for normal npm run lint) |
eslint.sonar.config.mjs |
Lightweight Sonar-only linting — used in watch mode alongside start:dev without heavy TypeScript project parsing |
Key features of this setup:
eslint-plugin-sonarjs enforces Sonar quality rules locally (no SonarQube server required during development)chokidar-cli — lints files automatically as you save them during developmentstart:dev)AGENTS.md teaches AI coding agents (Claude, Cursor, Copilot, etc.) to write Sonar-friendly code globallyeslint-plugin-sonarjs v4chokidar-clinpm install --save-dev \
eslint \
@typescript-eslint/parser \
@typescript-eslint/eslint-plugin \
eslint-plugin-sonarjs \
eslint-plugin-prettier \
eslint-config-prettier \
chokidar-cli \
husky \
lint-staged \
pretty-quick
Note:
chokidar-cliis critical for the watch-mode integration withstart:dev. Do not skip it.
Create eslint.config.js in the project root. This uses ESLint's flat config format (required for ESLint v9+).
// eslint.config.js
import js from '@eslint/js';
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import sonarjs from 'eslint-plugin-sonarjs';
export default [
js.configs.recommended,
{
ignores: ['dist/**', 'node_modules/**'],
},
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
parserOptions: {
project: ['./tsconfig.json'],
sourceType: 'module',
},
globals: {
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
},
},
plugins: {
'@typescript-eslint': tsPlugin,
sonarjs,
},
rules: {
...tsPlugin.configs.recommended.rules,
...sonarjs.configs.recommended.rules,
// TypeScript rule overrides
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-require-imports': 'off',
'no-duplicate-imports': 'error',
'@typescript-eslint/no-unused-vars': 2,
'@typescript-eslint/no-duplicate-enum-values': 2,
'no-undef': 'off',
// SonarJS rule overrides
'sonarjs/cognitive-complexity': ['warn', 20],
'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }],
'sonarjs/no-identical-functions': 'warn',
'sonarjs/no-collapsible-if': 'warn',
'sonarjs/prefer-immediate-return': 'warn',
'sonarjs/content-length': ['error', { fileUploadSizeLimit: 52428800, standardSizeLimit: 2000000 }],
},
},
];
Create eslint.sonar.config.mjs in the project root. This is a fast, lightweight config used in watch mode. It skips project: ['./tsconfig.json'] to avoid the slowness of full TypeScript type-checking on every file save.
// eslint.sonar.config.mjs
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import sonarjs from 'eslint-plugin-sonarjs';
export default [
{
ignores: ['dist/**', 'node_modules/**'],
linterOptions: {
reportUnusedDisableDirectives: false,
},
},
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
parserOptions: {
sourceType: 'module',
// No `project` here — intentionally omitted for speed in watch mode
},
globals: {
process: 'readonly',
console: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
},
},
plugins: {
'@typescript-eslint': tsPlugin,
sonarjs,
},
rules: {
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-require-imports': 'off',
...sonarjs.configs.recommended.rules,
},
},
];
Why two configs?
The maineslint.config.jsusesproject: ['./tsconfig.json']for full type-aware linting — but this is slow. The sonar watch config skips it so your dev workflow stays fast. The two configs complement each other: catch issues instantly in watch mode, and get the full picture onnpm run lint.
Add or update these scripts in your package.json:
"scripts": {
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
"lint:sonar": "eslint --config eslint.sonar.config.mjs \"{src,apps,libs,test}/**/*.ts\"",
"lint:sonar:watch": "chokidar \"src/**/*.ts\" \"test/**/*.ts\" -c \"npm run lint:sonar\" --initial",
"lint:changed": "git diff --name-only --diff-filter=ACMR HEAD | grep -E '\\.(ts)