SonarLint Integration Guide — NestJS + TypeScript

A complete guide to the SonarLint/SonarJS setup used in this project, and how to replicate it in any NestJS + TypeScript project from scratch.


Table of Contents

  1. What Was Implemented
  2. Tech Stack
  3. Step-by-Step Setup for Your Project
  4. Script Reference
  5. Rules Explained
  6. AGENTS.md — Making AI Coding Agents Sonar-Aware
  7. Bonus: Running SonarLint Alongside Your Dev Server
  8. FAQ

What Was Implemented

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:


Tech Stack


Step-by-Step Setup for Your Project

1. Install Dependencies

npm 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-cli is critical for the watch-mode integration with start:dev. Do not skip it.


2. Create the Main ESLint Config

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 }],
    },
  },
];

3. Create the Sonar-Only ESLint Config

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 main eslint.config.js uses project: ['./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 on npm run lint.


4. Update package.json Scripts

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)
    
| xargs -r eslint" }
Script When to use
npm run lint Full lint with TypeScript project context (CI, before release)
npm run lint:sonar Sonar-only lint on all files (quick full scan)
npm run lint:sonar:watch Sonar lint in watch mode — re-runs on every file save
npm run lint:changed Lint only git-changed files (used in pre-commit hook)

5. Integrate with start:dev (Watch Mode)

This is the most powerful feature of this setup.

Update your start:dev script to run SonarLint in parallel with the NestJS dev server:

"start:dev": "npm run lint:sonar:watch & NODE_ENV=development nest start --debug=9224 --watch"

The & operator runs both processes concurrently in the same terminal session:

What this means for developers:

When you run npm run start:dev, you get:

  1. 🚀 Your NestJS server auto-reloading on changes
  2. 🔍 Sonar quality issues showing up in your terminal as you type, without leaving your dev loop

You don't need a SonarQube server. You don't need a separate terminal. Issues surface immediately.


6. Set Up Husky Pre-commit Hook

Initialize Husky:

npx husky init

Set the pre-commit hook to lint only changed TypeScript files:

# .husky/pre-commit
npm run lint:changed

Or if you prefer lint-staged, add to package.json:

"lint-staged": {
  "*.ts": ["eslint --fix", "git add"]
}

And update the Husky hook:

# .husky/pre-commit
npx lint-staged

The lint:changed script uses git diff to find only modified files, making pre-commit checks fast even in large codebases.


Script Reference

npm run lint                  → Full lint (TypeScript-aware, all files)
npm run lint:sonar            → Sonar rules only (all files, fast)
npm run lint:sonar:watch      → Sonar rules in watch mode (auto re-runs on save)
npm run lint:changed          → Lint only git-diff changed TS files (pre-commit)
npm run start:dev             → Dev server + Sonar watch running in parallel

Rules Explained

SonarJS Rules in Use

Rule Severity What It Catches
sonarjs/cognitive-complexity warn (threshold: 20) Functions that are too deeply nested or branched — hard to read and test
sonarjs/no-duplicate-string warn (threshold: 5) String literals repeated 5+ times — should be extracted to a constant
sonarjs/no-identical-functions warn Two functions with identical bodies — extract a shared helper
sonarjs/no-collapsible-if warn if blocks that can be merged — simplify with &&
sonarjs/prefer-immediate-return warn Unnecessary temp variable before return
sonarjs/content-length error File upload or request body exceeding safe size limits

All other sonarjs.configs.recommended.rules are also active. These cover SQL injection risk patterns, dead code, always-false conditions, and more.

TypeScript Rules in Use

Rule Severity Notes
@typescript-eslint/no-unused-vars error No dead variables
@typescript-eslint/no-duplicate-enum-values error Enum integrity
no-duplicate-imports error Keep imports clean
@typescript-eslint/no-explicit-any off Relaxed for practical NestJS usage

AGENTS.md — Making AI Coding Agents Sonar-Aware

This is the bonus tip and arguably the most impactful part of this setup.

What is AGENTS.md?

AGENTS.md is a special file that AI coding agents (Claude Code, Cursor, GitHub Copilot Workspace, etc.) automatically read at the start of every session. It acts as a global system prompt for the agent — injecting your team's coding standards directly into the AI's behaviour without requiring any prompting from the developer.

This project's AGENTS.md includes a Sonar-friendly coding policy section:

## Sonar-friendly coding policy (global)

All coding agents should write code that is friendly to SonarQube/SonarCloud quality checks by default.

### Required practices

1. Keep functions focused and reasonably small; avoid high cognitive complexity.
2. Avoid duplicated logic and duplicated string literals when they can be centralized.
3. Prefer early returns over deeply nested `if/else` blocks.
4. Avoid identical/near-identical function bodies; extract reusable helpers.
5. Handle `null`/`undefined` explicitly and avoid unsafe assumptions.
6. Keep DTO/service/controller boundaries clear and single-purpose.
7. Use meaningful names for methods/variables; avoid ambiguous abbreviations.
8. Do not silence lint/sonar rules unless there is a documented, justified reason.

### Regex and ReDoS prevention

- Avoid regex patterns with nested quantifiers (`(a+)+`, `(.*)*`) — vulnerable to ReDoS.
- Prefer built-in string methods over regex when possible.
- If regex is necessary, keep patterns simple and anchored.

### Delivery checklist (before finalizing)

- Run lint locally and address SonarJS findings in touched files.
- If a rule is intentionally not followed, explain why in PR/commit notes.
- Prefer maintainable/refactor-friendly code over quick one-off implementations.

How It Was Used in This Project

Every session with Claude Code (the AI agent used in this project) automatically inherits these constraints. This means:

The result: AI-generated code in this codebase already passes the majority of SonarJS rules before the developer even runs the linter.

How to Add AGENTS.md to Your Project

Create AGENTS.md in your project root with at minimum:

# Coding Agent Guidelines

## Sonar-friendly coding policy (global)

All coding agents should write code that is friendly to SonarQube/SonarCloud quality checks by default.

### Required practices

1. Keep functions focused and reasonably small; avoid high cognitive complexity.
2. Avoid duplicated logic and duplicated string literals when they can be centralized.
3. Prefer early returns over deeply nested `if/else` blocks.
4. Avoid identical/near-identical function bodies; extract reusable helpers.
5. Handle `null`/`undefined` explicitly and avoid unsafe assumptions.
6. Keep DTO/service/controller boundaries clear and single-purpose.
7. Use meaningful names for methods/variables; avoid ambiguous abbreviations.
8. Do not silence lint/sonar rules unless there is a documented, justified reason.

### Delivery checklist (before finalizing)

- Run lint locally and address SonarJS findings in touched files.
- If a rule is intentionally not followed, explain why in PR/commit notes.
- Prefer maintainable/refactor-friendly code over quick one-off implementations.

AGENTS.md is read by: Claude Code, Cursor (as .cursorrules equivalent when placed in root), GitHub Copilot Workspace, Aider, OpenCode, and other agents that respect project-level instruction files.


Bonus: Running SonarLint Alongside Your Dev Server

This is the killer feature — and the reason chokidar-cli is in devDependencies.

How it works

npm run start:dev
      │
      ├── npm run lint:sonar:watch   (Process 1, runs in background)
      │         │
      │         └── chokidar watches src/**/*.ts and test/**/*.ts
      │                   └── on any file change → runs: npm run lint:sonar
      │
      └── nest start --debug=9224 --watch   (Process 2, your NestJS server)
                └── watches src/**/*.ts → recompiles on change

Both processes run in the same terminal using the shell & operator. When you save a file:

  1. NestJS recompiles and restarts the server
  2. Sonar linter re-scans your changed file and prints any quality issues

Terminal output example

[NestJS] Compiling... 
[Sonar]  src/trips/trips.service.ts
           12:5  warning  Cognitive Complexity of 22 exceeds 20  sonarjs/cognitive-complexity
[NestJS] Application is running on: http://localhost:3000

You fix the issue, save again, and both processes respond immediately.

Why this matters

Without this setup, developers would need to:

With this setup, quality feedback is real-time — the same way TypeScript type errors are real-time in your editor.


FAQ

Q: Do I need a SonarQube server for this to work?
No. eslint-plugin-sonarjs runs entirely locally as an ESLint plugin. No server, no token, no network required.

Q: Will this slow down my development server?
The watch linter (lint:sonar:watch) uses the lightweight eslint.sonar.config.mjs which skips full TypeScript project parsing. It runs fast enough to not interfere with the dev server.

Q: What's the difference between eslint.config.js and eslint.sonar.config.mjs?
eslint.config.js is the full config with TypeScript project context (slower, more accurate, used in CI/npm run lint). eslint.sonar.config.mjs is the lightweight watch-mode config (fast, Sonar rules only, used in start:dev).

Q: Can I use this without NestJS?
Yes. The ESLint and chokidar setup is framework-agnostic. Only the nest start part of start:dev is NestJS-specific. Replace that with ts-node-dev, nodemon, vite, or whatever your dev server command is.

Q: What version of ESLint is required?
ESLint v9+ is required for flat config format (eslint.config.js). If you're on ESLint v8, you need to use .eslintrc.js format instead and adjust the imports accordingly.

Q: Does AGENTS.md affect non-AI developers?
No. AGENTS.md is only read by AI coding agents. Human developers are unaffected — though they can read it as a coding standards reference too.