4 Tools to Preserve Your Code Quality
Code quality never drops suddenly. It erodes little by little, commit after commit, until certain files become no-go zones, synonymous with recurring bugs.
After launching dozens of projects, I've made an undeniable observation: the longer you wait to implement strict quality rules, the more technical debt accumulates. And no one ever has the time or desire to deal with it after the fact.
Code quality never drops suddenly. It erodes little by little, commit after commit, until certain files become no-go zones, synonymous with recurring bugs.
Today, I'm sharing proven tips that will allow you to maintain robust and consistent code quality from day one of your project while helping your team improve.
Which Tools to Control Code Quality?
1. TypeScript: The Infallible Guide
⚡ TL;DR: Detects errors before runtime and makes code self-documenting.
TypeScript transforms the development approach by shifting error detection from runtime to compilation. Strict mode, enabled via "strict": true, combines several crucial checks: noImplicitAny (forbids implicit any types), strictNullChecks (enforces null/undefined handling) and strictFunctionTypes (strengthens signature compatibility). This configuration eliminates most classic runtime errors and significantly improves the development experience.
Here are the preliminary rules I add by default in TSConfig:
// tsconfig.json
{
"compilerOptions": {
"target": "es2022",
"strict": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitOverride": true
}
}For example, noUncheckedIndexedAccess is one of my favorite rules. It forces me to handle cases where array[index] might be undefined, eliminating those terrible runtime errors Cannot read property of undefined.
// ❌ Without noUncheckedIndexedAccess
const user = users[0];
user.name; // Crash if users is empty
// ✅ With noUncheckedIndexedAccess
const user = users[0];
if (user) {
user.name; // TypeScript forces checking user existence
}Another example with the exactOptionalPropertyTypes rule that breaks the ambiguity of optional properties. Without this rule, TypeScript doesn't distinguish between a property that's absent and one that's undefined.
// ❌ Without exactOptionalPropertyTypes
interface User {
name?: string; // equivalent to name: string | undefined
}
const user: User = { name: undefined };
// ✅ With exactOptionalPropertyTypes
interface User {
name?: string; // optional, if defined then string only
}
const user: User = {}; // `name` cannot be set to undefinedWhat I've learned:
💡 TypeScript doesn't just prevent bugs: it lightens my cognitive load by making code explicit and predictable.
2. ESLint: The Team's Sensei
⚡ TL;DR: Automates best practices and elevates the entire team's level.
I long thought ESLint was only for detecting errors and writing clean code. In reality, it has another true power: reducing team friction by enforcing consistent standards. This tool saves me precious time during code reviews: instead of manually correcting dangerous patterns and inconsistencies, I can focus on architecture and business logic.
// eslint.config.mts
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['node_modules/**', 'public/**', '**/build/**', '**/dist/**', '**/coverage/**'],
},
eslint.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.{js,jsx,ts,tsx}'],
rules: {
// ...some example rules
'no-console': 'error',
'max-params': ['error', 3],
'func-style': ['error', 'declaration'],
'prefer-arrow-callback': 'error',
'react/jsx-no-literals': 'error',
},
},
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// ...some example rules
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/switch-exhaustiveness-check': 'error',
'@typescript-eslint/strict-boolean-expressions': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
}
);Let's take a concrete example with 'max-params': ['error', 3]. How many times have you seen (or written):
// ❌ 8 parameters: order to remember and difficult to maintain
function createUser(name, email, age, role, department, isActive, createdById, permissions) {
// Bug guaranteed as soon as two parameters of the same type are inverted
}
// ✅ 2 parameters: object-oriented and scalable design
function createUser(user, options) {
const { name, email, age, role, department, isActive, createdById } = user;
const { permissions } = options;
// Auto-completion, free order, native extensibility
}Another example with @typescript-eslint/switch-exhaustiveness-check:
type Status = 'pending' | 'approved' | 'rejected';
function handleStatus(status: Status) {
switch (status) {
case 'pending':
return 'Waiting';
case 'approved':
return 'Approved';
// ❌ ESLint detects the missing 'rejected' case
// Error: Switch is not exhaustive. Cases not matched: 'rejected'
}
}Or simply 'no-console': 'error' which prevents accidental leaks of sensitive data through debug logs and removes that "unfinished" code impression.
What I've learned:
💡 ESLint doesn't just correct potentially faulty code: it elevates the entire team's level by automating best practices.
3. Prettier: The Team Peacemaker
⚡ TL;DR: Eliminates formatting debates and speeds up code reviews.
// prettier.config.mjs
/**
* @see https://prettier.io/docs/configuration
* @type {import("prettier").Config}
*/
const config = {
plugins: ['prettier-plugin-tailwindcss'],
singleQuote: true,
trailingComma: 'es5',
tabWidth: 2,
printWidth: 100,
};
export default config;Instead of debating "is this PR well formatted?", the team focuses on "does this PR really solve the problem?". Prettier ensures that code follows the same writing style across all team members' IDEs. When Bob modifies Alice's file, the formatting remains identical: same indentation, same quotes, same trailing commas... The code review diff only reveals Bob's actual changes, without stylistic noise.
What I've learned:
💡 Prettier doesn't just eliminate sterile discussions: it frees mental energy for what truly adds value (architecture, business logic, side effects).
4. Husky: The Repository Bodyguard
⚡ TL;DR: Blocks errors before they reach Git.
# .husky/pre-commit
#!/bin/bash
lint-stagedHow many times has the build been broken in CI... without anyone noticing before the code review? How many poorly formatted commits could have been avoided with commitlint? How many "fix: typo" could have been automatically intercepted with codespell?
Husky is my local quality insurance. By leveraging Git hooks, it transforms human errors into technical impossibilities. Instead of discovering problems in CI/CD (often after several minutes of waiting), or worse, in production, Husky intercepts them instantly on your machine.
Without Husky ❌
$ git commit -m "feat: new order management"
✓ Commit successful
# 15 minutes later in CI...
❌ Build failed: TypeScript errors
❌ Tests failed: 3 tests broken
❌ Linting failed: console.log detected
# = 3 round trips, 45 minutes lostWith Husky ✅
$ git commit -m "feat: new order management"
⚡ Running pre-commit hooks...
✗ Type checking failed:
src/user.ts:42:5 - error TS2322: Type 'string' is not assignable to type 'number'
✗ ESLint found issues:
src/api.ts:15:9 - 'console.log' not allowed (no-console)
✗ 3 tests failed:
UserService › should validate email format
❌ Commit blocked - fix errors and try again
# = Error detected in 3 seconds, context still freshExample configuration:
# .husky/pre-commit
npx lint-staged// lint-staged.config.mts
import { type Configuration } from 'lint-staged';
const config: Configuration = {
'package.json': ['npx sort-package-json'],
'*.{ts,tsx}': [
'eslint --fix --max-warnings 0',
'prettier --write',
() => 'tsc --noEmit --skipLibCheck',
],
'*.md': ['prettier --write'],
'*.json': ['prettier --write'],
};
export default config;One use I particularly appreciate is automatic API contract validation. Thanks to tools like OpenAPI TypeScript, Husky can ensure that types generated from OpenAPI specifications are up to date and that the interface between frontend and backend remains consistent.
This approach allows immediate detection of breaking changes in APIs and desynchronizations between frontend/backend teams, before the code even reaches CI.
What I've learned:
💡 Husky doesn't just block defects: it guides the developer to fix detected issues as early as possible, while the context is still fresh in their memory.
Getting Started
If you're starting a new project, here's the survival kit:
# Installation in one command
yarn add -D \
typescript \
eslint \
typescript-eslint \
@eslint/js \
prettier \
eslint-config-prettier \
husky \
lint-staged
# For Next.js, also add:
yarn add -D \
eslint-config-next \
@eslint/eslintrc
# Initialization
npx husky init
npx tsc --init --strictThe Quality Domino Effect
These four tools form a continuous transformation chain before code deployment:
flowchart LR
Dev((Dev)) --> TS[🛡️ SECURE<br/>TypeScript]
TS --> ESLint[📚 TRAIN<br/>ESLint]
ESLint --> Prettier[🎨 HARMONISE<br/>Prettier]
Prettier --> Husky[✅ VALIDATE<br/>Husky]
Husky --> Git((Git))
style TS fill:#dbeafe,stroke:#3b82f6
style ESLint fill:#fef3c7,stroke:#f59e0b
style Prettier fill:#ede9fe,stroke:#8b5cf6
style Husky fill:#d1fae5,stroke:#10b981Each step reinforces the previous one:
- TypeScript detects errors while you code by ensuring type consistency
- ESLint enforces best practices by progressively raising the team's level
- Prettier harmonizes style by eliminating formatting debates
- Husky blocks errors before they reach Git by automating checks
The result? A team that naturally progresses, where each developer gradually improves not only the code but also their skills. These tools don't just correct: they transform the development culture by creating a virtuous circle of continuous improvement.
Measurable Impact on Your Projects
After setting up and properly configuring these tools on several projects, here are the improvements observed:
- Reduced debugging time: types catch errors before execution
- Faster code reviews: focus on logic, not style
- Reduced production bugs: majority intercepted before commit
- Faster onboarding: rules guide new developers
- Limited technical debt: quality maintained from day 1
These tools must be configured with strict rules from the start. The temptation to "start soft to not block the team" is a trap: you'll accumulate technical debt that no one will want to fix later.
Better a painful week of adaptation than a year of unmaintainable mediocre code.
The important thing is to set them up from the first commits (it's 100x harder to impose them on an existing codebase). Needs evolve over time, new ESLint rules appear, TypeScript adds new strict options. Take an hour per month to review your configuration, it won't be expensive.