
Best Practices for Angular.js Development

Angular has been around long enough that most teams have strong opinions about it. Some of those opinions are right. This post covers the practices we've found actually matter in production Angular applications, the ones that affect maintainability, performance, and how quickly a new engineer can get productive on a codebase.
How Should You Structure a Large Angular Application?
The answer depends on how your team works and how fast the codebase is expected to grow, but there are patterns that hold up consistently.
The most reliable structure we've seen is a feature-based module layout. Each feature owns its components, services, models, and routing. Shared utilities go into a SharedModule. App-wide singletons go into a CoreModule that is imported exactly once in AppModule and throws an error if imported again (a simple constructor guard handles this).
src/
app/
core/
shared/
features/
invoices/
components/
services/
invoices.module.ts
invoices-routing.module.ts
With Angular 14+, standalone components change this calculus somewhat. You no longer need NgModule declarations for every component, and tree-shaking becomes more effective. If you're starting a new project today, standalone components are worth the learning curve. If you're on a mature codebase, migrating piecemeal is feasible but needs a plan.
Lazy loading is not optional at scale
Every feature module should be lazy-loaded unless there is a concrete reason not to. A route that loads a module only when a user navigates to it can reduce initial bundle size dramatically. In projects with 40+ feature modules, we've seen initial bundle sizes drop from over 2MB to under 400KB with consistent lazy loading and proper tree-shaking.
Use loadChildren with dynamic imports rather than the older string-based syntax. The Angular compiler will warn you if the path is wrong at build time.
What Actually Causes Angular Performance Problems?
Change detection is where most Angular performance issues live. Angular's default ChangeDetectionStrategy.Default checks every component in the tree on every event. For small apps this is fine. For apps with large component trees and frequent data updates, it becomes a bottleneck.
Switch to ChangeDetectionStrategy.OnPush for components that receive data purely through @Input(). This tells Angular to only check that component when its inputs change, an event originates from it, or you explicitly mark it dirty via ChangeDetectorRef. The trade-off is that you have to be deliberate about when state updates flow through; unexpected UI staleness is the most common bug teams hit when first adopting OnPush.
RxJS subscriptions and memory leaks
Unmanaged subscriptions are the most common source of memory leaks in Angular. The async pipe handles unsubscription automatically, which is why it should be the default for template subscriptions. When you must subscribe imperatively in a component, use takeUntilDestroyed() (introduced in Angular 16) or the older Subject-based destroy pattern.
// Angular 16+
someObservable$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(value => this.handle(value));
Avoid subscribe() chains nested inside subscribe(). That's a sign you need switchMap, mergeMap, or concatMap.
Bundle size
Use @angular-devkit/build-angular with budgets configured in angular.json. Set a warning threshold and an error threshold for your initial bundle and component styles. This turns bundle bloat into a CI failure rather than a surprise in production.
/// Not sure where to start?
Get the architecture before you commit
Tell us what you're building and we'll map the technical approach, stack, and rough timeline. No cost, no obligation, no sales call required.
State Management: When Do You Actually Need NgRx?
NgRx is a good solution for genuinely complex shared state. It has real costs: boilerplate, a learning curve, and indirection that can make simple things harder to trace. Not every Angular app needs it.
Use a plain service with a BehaviorSubject for state that is local to a feature or shared between a small number of components. It is easier to test, easier to read, and has zero additional dependencies.
Reach for NgRx (or the lighter NgRx Signal Store, introduced in NgRx 17) when you have state that is shared across many unrelated features, when you need time-travel debugging, or when you need a strict audit trail of state mutations. The Redux DevTools integration alone makes NgRx worthwhile for financial or compliance-heavy applications where you need to reproduce exact states.
| Scenario | Recommended Approach |
|---|---|
| Single feature, few components | Service + BehaviorSubject |
| Multiple features sharing state | NgRx Store or Signal Store |
| Server state (fetch, cache, sync) | NgRx Data or a custom abstraction |
| Simple form state | Reactive Forms + local service |
Testing Strategy That Does Not Slow the Team Down
Angular's TestBed is powerful but slow. A test suite that takes 10 minutes to run locally is a test suite that developers stop running locally. Keep unit tests fast by avoiding TestBed where possible.
Pure functions, services without DOM dependencies, and RxJS pipelines can all be tested with plain Jasmine or Jest without TestBed. Reserve TestBed for component tests that genuinely need template rendering, dependency injection, or directive interaction.
For end-to-end tests, Cypress is more stable than Protractor (which is officially deprecated as of Angular 15). Use Cypress for critical user flows and keep the suite small. A suite of 30 well-chosen E2E tests beats 300 brittle ones.
Code coverage is a metric, not a goal. A component with 90% coverage that has no tests for its error states is not well-tested. Write tests for the paths that break in production, not for the paths that give you a green coverage badge.
Enforcing Code Quality Across a Team
Linting rules are agreements encoded in configuration. ESLint with @angular-eslint is the standard toolchain now that TSLint is deprecated. The @angular-eslint ruleset covers Angular-specific patterns: no unused lifecycle hooks, consistent component selector prefixes, enforced OnPush across the codebase if that's your standard.
Combine this with Prettier for formatting. Separate "style" from "correctness" in your ESLint config so that Prettier handles whitespace and ESLint handles logic. This eliminates an entire category of code review comment.
Strict TypeScript mode ("strict": true in tsconfig.json) is non-negotiable on any new project. It catches a class of bugs at compile time that would otherwise reach production. If you are on an existing codebase without strict mode, migrate incrementally using strictNullChecks first, then add the remaining flags.
Conclusion
The practices above are not prescriptions. They are defaults that have held up across multiple production codebases. Apply OnPush change detection and lazy loading from the start. Be conservative about NgRx until you actually need it. Keep tests fast and focused. Enforce standards through tooling rather than process.
The next concrete step: if you are starting a new Angular project, run ng new with --strict and --standalone flags and configure your bundle budgets in angular.json before you write a single component. If you are on an existing project, run ng update to check your current version gap and address it. Angular's official update guide at update.angular.io gives you an exact migration path for every version jump.
FAQ
Is AngularJS (1.x) still worth using in 2024?
No. AngularJS reached end-of-life in December 2021. Security patches stopped with that release. Any production application still running AngularJS carries real security risk. The migration path to Angular 2+ requires a rewrite, but Angular provides an upgrade module to run both versions in parallel during transition.
Should new Angular projects use standalone components or NgModules? For greenfield projects, standalone components are now the Angular team's recommended default as of Angular 17. They reduce boilerplate and improve tree-shaking. NgModules still work and are not deprecated, so large existing codebases do not need to migrate immediately.
When should you use Angular Signals instead of RxJS?
Signals, introduced in Angular 16 as developer preview and stabilised in Angular 17, are suited for synchronous reactive state inside components. RxJS remains the better choice for asynchronous operations, complex event stream composition, and HTTP coordination. The two work together: toSignal() and toObservable() bridge them.
How do you handle authentication tokens securely in an Angular app?
Store tokens in memory rather than localStorage where possible. localStorage is accessible to any JavaScript on the page, which makes it an XSS target. Use an HttpInterceptor to attach tokens to outgoing requests. For refresh token flows, keep the refresh token in an HttpOnly cookie managed by your backend.
What is the minimum Angular version a team should be on?
Angular 15 is the practical floor if you want access to stable standalone APIs and the modern @angular-eslint toolchain. Angular 16 and 17 add Signals and takeUntilDestroyed(). Angular releases a major version roughly every six months, and each major is supported with patches for 18 months, so staying within two major versions of the latest release is manageable.
Have a project in mind? Contact Sodio Technologies to discuss your requirements and explore the right technology solution for your business.
/// Work with us
Talk to the engineers who'd build it
You'll get a technical scope, timeline and cost estimate from the people doing the work, not an account manager. In-house team, no subcontracting, since 2016.
