Shipping bugs doesn't usually happen because developers are careless. It happens because teams rely on individual discipline instead of enforced systems.
Most teams are made up of developers with different backgrounds, learning paths, and mental models. That diversity is valuable — but without strict guardrails, it turns codebases into negotiation spaces. One developer tolerates any, another ignores lint warnings, someone else promises to "fix it later" and actually believes it. The product absorbs all of it.
Early in your career, this feels normal. You assume structure will emerge organically. It won't.
We've seen this story before with TypeScript itself. Many of us resisted it at first, complained about friction, and eventually realized it wasn't slowing us down — it was preventing entire classes of bugs. The same pattern repeats with every serious quality boundary we avoid enforcing.
I've started projects where junior developers defaulted to any, enabled ESLint only to silence it, skipped formatting rules, and treated lint errors as optional. Not because they were careless — but because the system quietly permitted it.
These are not people's problems. They are system failures.
When you design a product, you deliberately choose constraints. Code quality deserves the same level of intent. Rules should be explicit, enforced early, and impossible to bypass.
In this article, I'll walk through the tooling and discipline we use in Angular to make code predictable, scalable, and unemotional about quality — so mistakes are caught immediately, not negotiated in pull requests later.
Why Angular needs stricter enforcement than most frontend stacks
Angular is not fragile, but it is forgiving in dangerous ways.
On the surface, Angular feels structured: modules, decorators, dependency injection, lifecycle hooks. That apparent structure gives teams a false sense of safety. People assume the framework will "handle things" for them. It won't.
Angular applications hide complexity behind abstractions. A small mistake rarely fails loudly. It degrades quietly.
Templates are a second language. They look like HTML, but they behave like TypeScript — except the compiler won't save you unless you force it to. Without strict template checking, it's easy to ship runtime errors that no test or build step ever caught.
RxJS amplifies this problem. Subscriptions don't fail immediately when misused. They leak memory, duplicate side effects, and introduce timing bugs that only appear under load. Angular won't stop you from subscribing incorrectly. It will happily let the problem grow.
Dependency injection adds another layer of indirection.
A service with loose typing or unsafe assumptions spreads risk across the entire application. One careless any doesn't stay local — it infects everything downstream.
None of this makes Angular bad. It makes it powerful. But power without enforcement is how technical debt becomes invisible.
In loosely structured stacks, mistakes are obvious early. In Angular, mistakes age quietly. By the time they hurt, they're baked into templates, services, and shared abstractions.
This is why Angular teams cannot rely on "best practices" as guidelines. They have to be rules. Enforced at build time. Enforced before code reaches review. Enforced before humans are asked to notice what tools could have caught instantly.
Tools don't create discipline — enforcement does
Most frontend teams already use the "right" tools.
TypeScript is enabled. ESLint exists. Prettier is installed. Tests run sometimes.
And yet the same problems persist.
That's because tools, by themselves, are passive. They suggest. They warn. They politely complain. Developers can ignore all of it and still ship code.
Discipline appears only when the system removes choice.
In our setup, tools are not helpers. They are gates. Each one exists at a specific stage of development, with a specific job: catch a class of mistakes as early as possible and stop the process immediately.
This isn't about trust. It's about economics. Fixing mistakes earlier is cheaper than discussing them later.
A note on configuration
This article focuses on why these constraints matter and how they work together as a system.
Full configuration files are intentionally not inlined here. They're verbose, change over time, and are better consumed directly. Complete setup references and links are provided at the end for anyone who wants to apply this approach in their own Angular project.
TypeScript strictness: the non-negotiable foundation
Before adding a single tool, strict typing has to be mandatory.
If strict mode is off, everything else is cosmetic. ESLint becomes advisory. Reviews turn subjective. Bugs escape silently.
Strict TypeScript forces the codebase to answer uncomfortable questions early:
- What can actually be
null? - What assumptions are unsafe?
- Which APIs are lying about their contracts?
Turning strict mode on late feels painful because it exposes debt that was already there. That pain isn't a tooling issue — it's feedback.
In Angular, this also means enabling strict template type checking. Without it, a large portion of your application is effectively untyped. Runtime template errors become inevitable, and debugging them is far more expensive than preventing them.
Strictness doesn't slow teams down. It removes ambiguity — and ambiguity is where bugs hide.
ESLint: from style checker to design enforcer
Most teams stop too early with ESLint.
Used poorly, ESLint argues about formatting and naming. Used correctly, it enforces architecture.
In Angular, this means rules that:
- Block
anyand unsafe type assertions - Prevent forgotten subscription teardowns
- Catch floating promises and ignored async work
- Enforce predictable RxJS patterns
- Fail builds when unsafe assumptions leak into components or templates
If ESLint errors are allowed to pass "just this once", the system is already compromised.
ESLint is not there to nag developers. It exists so humans don't have to.
Prettier: eliminating meaningless choice
Formatting is not a quality signal. It's noise.
Prettier exists to eliminate that noise completely. No preferences. No debates. No pull-request comments about spacing.
It runs automatically, before code is committed. If formatting ever fails in CI, something upstream is broken.
This isn't about aesthetics. It's about preserving attention for problems that actually matter.
Husky and pre-commit hooks: enforcing standards at the only time that matters
Code review is too late to catch basic mistakes.
By the time a human sees the code, the system has already failed. At that point, the only options left are comments, rewrites, and follow-up commits — all avoidable work.
Pre-commit hooks move feedback to the moment code is written. When ESLint fails or formatting is wrong, the commit simply doesn't happen. There's no discussion, no negotiation, no "I'll fix it later". The system enforces the rule.
The key idea is graduated enforcement.
Not every check belongs at the same stage.
Fast, local feedback should happen continuously while writing code. Slightly heavier checks should block commits. The strictest checks should block pushes and fail CI. Each layer exists to catch mistakes at the cheapest possible point.
Continuous feedback while coding
Waiting until commit time is already late.
Linting should run while the developer is typing, not after they think they're done. This is where watch mode matters.
For example, running Angular and ESLint together:
{
"scripts": {
"dev": "ng lint && ng serve"
}
}
This ensures the application doesn't even start if linting fails. The feedback is immediate.
For teams that want even tighter loops, ESLint can run independently in watch mode alongside the dev server:
{
"scripts": {
"dev": "concurrently \"ng serve\" \"eslint . --ext .ts,.html --watch\""
}
}
This turns linting into a live signal instead of a delayed punishment. Errors appear while code is being written, not after context has already shifted.
This is especially effective when combined with Angular template linting and RxJS rules, where subtle mistakes are easy to miss and expensive to debug later.
Pre-commit: fast, unavoidable checks
Pre-commit hooks exist to stop bad code from entering history.
At this stage, only fast checks belong here:
- ESLint on changed files
- Prettier formatting
Nothing slow. Nothing flaky.
If a commit fails, the fix should take seconds, not minutes. Otherwise, developers will bypass the system — and a bypassable system is already broken.
This is where tools like Husky and lint-staged matter, not as setup details, but as enforcement mechanisms. The rule is simple: If the code doesn't meet the baseline, it doesn't get committed.
Pre-push and CI: stricter, slower, final
More expensive checks belong later.
Before pushing — and again in CI — the system can afford to be strict:
- Full
ng lint - Type checking with
tsc --noEmit - Tests
- Build validation
At this point, failure is intentional. It protects the shared branch, not the individual developer.
Importantly, CI should never be the first place an error is discovered. If CI catches issues developers never saw locally, feedback loops are too slow.
RxJS and Angular-specific enforcement
This setup becomes far more valuable when combined with Angular- and RxJS-specific lint rules.
Rules from eslint-plugin-rxjs and @angular-eslint/schematics catch issues humans routinely miss:
- Forgotten subscription teardowns
- Nested subscriptions
- Unsafe async flows
- Template misuse that compiles but fails at runtime
These are not stylistic preferences. They are bug prevention mechanisms.
Once enforced at commit time, these mistakes simply stop happening.
Commit discipline: code is not the only artifact
Quality doesn't stop at source files.
Commit history is part of the system. Vague messages like "fix", "final", or "working now" destroy context, slow debugging, and make releases harder than they need to be.
Structured commit messages turn history into documentation. They make intent explicit long after the code has changed.
This isn't bureaucracy. It's professional hygiene.
References and setup
- Angular strict mode and strict templates
- @angular-eslint and @typescript-eslint/strict
- RxJS ESLint rules
- Prettier
- Husky, pre-commit and lint-staged
- Commitlint and Conventional Commits
If you enjoyed reading this, a clap, follow, or comment would mean a lot. It literally takes a minute, but it's motivating for me to write more content like this.
Author: Ayush Maurya
Top comments (0)