Linting and Code Quality
This guide covers the linting, formatting, and code quality tools used in the Awesome NestJS Boilerplate.
- Linting and Code Quality
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):
{
"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:
PascalCasefor classes, types, enumscamelCasefor variables and functionsSCREAMING_SNAKE_CASEfor constants and enum memberskebab-casefor file names
Running Biome
# Check all files (lint + format)
pnpm exec biome check .
# Fix auto-fixable issues
pnpm exec biome check --write .
# Check only staged files
pnpm lint:changesESLint (Supplementary)
ESLint runs after Biome for rules that Biome does not cover (import ordering, security scanning, Node.js-specific rules, etc.).
Installed Plugins
{
"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 (useunknowninstead)simple-import-sort/imports+simple-import-sort/exports: enforces sorted importsno-secrets/no-secrets: prevents committing API keys/tokenssonarjs/cognitive-complexity: limits cyclomatic complexityunicorn/prefer-node-protocol: enforcesnode:prefix for built-in importspromise/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:
// 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):
{
"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: Requiresimport typefor type-only importsallowImportingTsExtensions: true: Allows.tsextensions in import paths (required for ESM)noUncheckedIndexedAccess: true: Array/object access returnsT | undefinedisolatedModules: true: Compatible with single-file transpilation
ESM imports must include .ts extension:
// ✅ 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:
pnpm exec lint-stagedLint-Staged
package.json lint-staged configuration:
{
"lint-staged": {
"*.ts": [
"npx @biomejs/biome lint --write",
"eslint --fix",
"git add"
]
}
}Execution order on every commit:
- Biome auto-fixes formatting and lint issues on staged
.tsfiles - ESLint fixes remaining issues (import ordering, security, etc.)
- 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
# 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:changesNote: 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):
{
"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
Enable ESLint:
- Go to
Settings > Languages & Frameworks > JavaScript > Code Quality Tools > ESLint - Check "Automatic ESLint configuration"
- Enable "Run eslint --fix on save"
- Go to
Enable Biome:
- Install the Biome plugin
- Go to
Settings > Languages & Frameworks > Biome - Enable "Use Biome as formatter"
Code Quality Rules
Naming Conventions
// ✅ 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
// ✅ 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
// ✅ 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:
// ❌ 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.mjsvia TypeScript-specific overrides.
ESLint not working in IDE:
# 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.tsImport resolution errors:
# Verify TypeScript paths
pnpm exec tsc --noEmit --listFilesSlow linting:
# Use ESLint cache
pnpm lint --cacheBest 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
// ✅ 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:
- name: Lint
run: pnpm lint
- name: Build
run: pnpm build:prod