Skip to content

Linting and Code Quality

This guide covers the linting, formatting, and code quality tools used in the Awesome NestJS Boilerplate.

Overview

The project uses a two-layer linting and formatting setup:

  • Biome: Primary formatter and linter (runs first — handles most code style and formatting)
  • ESLint: Supplementary linter for rules not covered by Biome (runs after Biome)
  • TypeScript: Strict type checking (strict: true, verbatimModuleSyntax, noUncheckedIndexedAccess)
  • Husky + Lint-staged: Git hooks that run both tools on staged files before every commit

There is no standalone Prettier — Prettier runs only as an ESLint plugin rule (prettier/prettier).

Biome (Primary)

Biome is the primary code formatter and linter. It replaces Prettier as the standalone formatter and handles many lint rules faster than ESLint.

Configuration

biome.json (key settings):

json
{
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true
    }
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single"
    }
  }
}

Biome enforces naming conventions configured in biome.json:

  • PascalCase for classes, types, enums
  • camelCase for variables and functions
  • SCREAMING_SNAKE_CASE for constants and enum members
  • kebab-case for file names

Running Biome

bash
# Check all files (lint + format)
pnpm exec biome check .

# Fix auto-fixable issues
pnpm exec biome check --write .

# Check only staged files
pnpm lint:changes

ESLint (Supplementary)

ESLint runs after Biome for rules that Biome does not cover (import ordering, security scanning, Node.js-specific rules, etc.).

Installed Plugins

json
{
  "devDependencies": {
    "@eslint/js": "^9.x",
    "@typescript-eslint/eslint-plugin": "^8.x",
    "@typescript-eslint/parser": "^8.x",
    "eslint": "^9.x",
    "eslint-plugin-canonical": "^5.x",
    "eslint-plugin-import": "^2.x",
    "eslint-plugin-import-helpers": "^2.x",
    "eslint-plugin-n": "^17.x",
    "eslint-plugin-no-secrets": "^2.x",
    "eslint-plugin-prettier": "^5.x",
    "eslint-plugin-promise": "^7.x",
    "eslint-plugin-simple-import-sort": "^12.x",
    "eslint-plugin-sonarjs": "^3.x",
    "eslint-plugin-unicorn": "^59.x"
  }
}

Configuration File

eslint.config.mjs uses ESLint 9 flat config format. Key rules enforced:

  • @typescript-eslint/no-explicit-any: error (use unknown instead)
  • simple-import-sort/imports + simple-import-sort/exports: enforces sorted imports
  • no-secrets/no-secrets: prevents committing API keys/tokens
  • sonarjs/cognitive-complexity: limits cyclomatic complexity
  • unicorn/prefer-node-protocol: enforces node: prefix for built-in imports
  • promise/catch-or-return: ensures promises are handled

TypeScript rules that overlap with Biome or the TS compiler are disabled to avoid duplicate errors.

Prettier via ESLint

There is no .prettierrc file in the project. Prettier formatting rules are applied through eslint-plugin-prettier:

javascript
// Inline Prettier config in eslint.config.mjs
{
  'prettier/prettier': ['error', {
    singleQuote: true,
    trailingComma: 'all',
    tabWidth: 2,
    bracketSpacing: true,
  }]
}

TypeScript Configuration

Compiler Options

tsconfig.json (key settings):

json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Node",
    "lib": ["ESNext"],
    "strict": true,
    "strictNullChecks": true,
    "noImplicitAny": true,
    "noUnusedParameters": true,
    "noUnusedLocals": true,
    "noUncheckedIndexedAccess": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "isolatedModules": true,
    "baseUrl": "./src",
    "outDir": "./dist"
  }
}

Strict Mode

The project enables all TypeScript strict flags plus additional safety checks:

  • strict: true: Enables all strict checks (strictNullChecks, noImplicitAny, etc.)
  • verbatimModuleSyntax: true: Requires import type for type-only imports
  • allowImportingTsExtensions: true: Allows .ts extensions in import paths (required for ESM)
  • noUncheckedIndexedAccess: true: Array/object access returns T | undefined
  • isolatedModules: true: Compatible with single-file transpilation

ESM imports must include .ts extension:

typescript
// ✅ Correct
import { UserService } from './user.service.ts';
import type { UserDto } from './user.dto.ts';

// ❌ Wrong
import { UserService } from './user.service';

Git Hooks

Husky Configuration

.husky/pre-commit runs lint-staged on every commit:

bash
pnpm exec lint-staged

Lint-Staged

package.json lint-staged configuration:

json
{
  "lint-staged": {
    "*.ts": [
      "npx @biomejs/biome lint --write",
      "eslint --fix",
      "git add"
    ]
  }
}

Execution order on every commit:

  1. Biome auto-fixes formatting and lint issues on staged .ts files
  2. ESLint fixes remaining issues (import ordering, security, etc.)
  3. Fixed files are re-staged automatically

This means any commit that passes the hook is guaranteed to meet both Biome and ESLint standards.

Available Scripts

bash
# Run ESLint on all files
pnpm lint

# Run ESLint with auto-fix
pnpm lint:fix

# Check staged files with Biome (pre-commit equivalent)
pnpm lint:changes

Note: To run Biome manually on all files, use pnpm exec biome check --write .

IDE Integration

VS Code Setup

Install the Biome VS Code extension and the ESLint extension.

.vscode/settings.json (recommended):

json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "biomejs.biome",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "quickfix.biome": "explicit"
  },
  "eslint.validate": ["typescript"],
  "typescript.preferences.importModuleSpecifier": "relative"
}

WebStorm Setup

  1. Enable ESLint:

    • Go to Settings > Languages & Frameworks > JavaScript > Code Quality Tools > ESLint
    • Check "Automatic ESLint configuration"
    • Enable "Run eslint --fix on save"
  2. Enable Biome:

    • Install the Biome plugin
    • Go to Settings > Languages & Frameworks > Biome
    • Enable "Use Biome as formatter"

Code Quality Rules

Naming Conventions

typescript
// ✅ PascalCase for classes
export class UserService {}

// ✅ camelCase for variables and functions
const userName = 'john';
function getUserById(id: string) {}

// ✅ kebab-case for file names
// user-service.ts, create-user.dto.ts

// ✅ SCREAMING_SNAKE_CASE for constants and enum values
const MAX_RETRY_ATTEMPTS = 3;
enum RoleType { USER = 'USER', ADMIN = 'ADMIN' }

Import Organization

typescript
// ✅ Correct — .ts extension required for ESM
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { UserEntity } from '../../modules/user/user.entity.ts';
import type { CreateUserDto } from '../dtos/create-user.dto.ts';

// ❌ Wrong — missing .ts extension
import { UserEntity } from '../../modules/user/user.entity';

Code Complexity

typescript
// ✅ Low cognitive complexity
function processUser(user: User): ProcessedUser {
  if (!user.isActive) {
    return { ...user, status: 'inactive' };
  }

  if (user.role === RoleType.ADMIN) {
    return { ...user, permissions: getAdminPermissions() };
  }

  return { ...user, permissions: getUserPermissions() };
}

// ❌ High cognitive complexity (too many nested conditions)
function processUser(user: User): ProcessedUser {
  if (user.isActive) {
    if (user.role === 'admin') {
      if (user.department === 'IT') {
        if (user.experience > 5) {
          // Too deeply nested
        }
      }
    }
  }
}

Security Linting

The no-secrets plugin prevents committing sensitive information:

typescript
// ❌ Bad — hardcoded secrets will fail the lint check
const apiKey = 'sk-1234567890abcdef';
const password = 'mySecretPassword123';

// ✅ Good — use environment variables
const apiKey = process.env.API_KEY;
const password = process.env.DATABASE_PASSWORD;

Troubleshooting

Common Issues

Biome and ESLint rules conflict:

  • Biome takes precedence. ESLint rules that duplicate Biome's formatting rules are disabled in eslint.config.mjs via TypeScript-specific overrides.

ESLint not working in IDE:

bash
# Restart TypeScript service in VS Code
# Ctrl+Shift+P > "TypeScript: Restart TS Server"

# Verify ESLint can parse the file
pnpm exec eslint --debug src/main.ts

Import resolution errors:

bash
# Verify TypeScript paths
pnpm exec tsc --noEmit --listFiles

Slow linting:

bash
# Use ESLint cache
pnpm lint --cache

Best Practices

1. Let the hooks do the work

Commit normally — Husky + lint-staged will auto-fix Biome and ESLint issues before the commit lands. You only need to run lint manually when debugging.

2. Prefer import type for type-only imports

verbatimModuleSyntax enforces this. IDE auto-imports will often get it right; lint-staged fixes it otherwise.

3. Document ESLint disable comments

typescript
// ✅ Good — explain why the disable is needed
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// TODO: Replace with proper typing after API update — see issue #123
const legacyData: any = await getLegacyData();

// ❌ Bad — no explanation
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = getData();

4. CI/CD Integration

Run linting in CI before building:

yaml
- name: Lint
  run: pnpm lint

- name: Build
  run: pnpm build:prod