The Angular most people still argue about on the internet stopped existing a few versions ago. The framework that made you write an NgModule for everything, decorate every input, and trust zone.js to guess when to re-render is gone. What replaced it is signals, standalone by default, and as of version 21, no zone.js at all. If your mental model of Angular is the one from the AngularJS days, or honestly even from 2022, you are arguing with a ghost.

This is how we build Angular at 2muchcoffee in 2026. Not the version the old tutorials teach. The current one. Years of writing it in production, distilled into the rules we actually follow, with the code to back each one.

angular, then vs now · click a topic
then
now

Why this manifesto exists

Angular ships a major version about every six months, and it has spent the last three years quietly replacing itself. A developer who learned Angular in 2021 and stopped paying attention is now writing a dialect the framework actively discourages. That gap is not cosmetic. It shows up as slower apps, bigger bundles, more bugs, and code that a new hire cannot read because it looks nothing like the docs. A manifesto is how we keep a team writing the same current Angular instead of five personal versions of it. We keep one for our backend stack on the same principle, the 2muchcoffee Nest.js manifesto. Below is what current means, section by section.

Architecture: you stopped needing NgModules

Start every project with the CLI. ng new gives you standalone components and zoneless change detection by default now, no flags. It standardizes setup and removes the boilerplate nobody enjoys. Structure by feature, not by type: locate code fast, keep each piece isolated, stay flat instead of nesting five folders deep, and stay DRY without inventing abstractions you do not need yet.

project structure
src/app/
  core/        # singletons: interceptors, guards
  shared/      # reusable, standalone UI
  features/    # one folder per feature
  app.routes.ts

The big one: NgModules are legacy. Standalone has been the default since v19, so a component imports exactly what it needs and nothing else. You get a flat dependency graph, simpler lazy loading, and a faster cold start. If you still reach for SharedModule and CoreModule on new code, you are doing 2019 in 2026.

✗ then · user.module.ts
@NgModule({
  declarations: [UserCardComponent],
  imports: [CommonModule],
  exports: [UserCardComponent],
})
export class UserModule {}
✓ now · user-card.component.ts
@Component({
  selector: 'user-card',
  imports: [AvatarComponent],
})
export class UserCardComponent {}

Naming stays boring on purpose: kebab-case files and selectors, .component.ts / .service.ts suffixes, PascalCase classes with no I prefix, camelCase members, streams end with $, booleans start with is / has / can, and handlers are named by purpose, not by the event that triggered them.

The component: signals all the way down

Use the signal-based inputs and outputs, not the decorators. input(), input.required<T>(), output(), and model() for two-way are reactive, type-safe, and need none of the setter-getter hacks @Input() forced on you. Inject dependencies with inject(): cleaner constructor, tree-shakable, works in plain functions, not just classes.

✗ then
@Input() name!: string;
@Output() saved = new EventEmitter<User>();

constructor(private users: UserService) {}
✓ now
readonly name = input.required<string>();
readonly saved = output<User>();

private readonly users = inject(UserService);

Mark what should not change. readonly on injected services, inputs, outputs, and constants. protected on anything the template binds to, so it is clear the property exists only for the view. There is a member order we keep so any file reads the same way: static properties, then inputs and outputs, then injected dependencies, then internal state, then accessors, then the constructor (usually omitted), then lifecycle hooks, then public methods, then private ones.

user-info.component.ts
@Component({ selector: 'user-info', imports: [AvatarComponent] })
export class UserInfoComponent {
  // 1. static
  static readonly fallback = 'assets/default-avatar.png';

  // 2. inputs / outputs (signal-based)
  readonly user = input.required<User>();
  readonly opened = output<void>();

  // 3. injected deps
  private readonly users = inject(UserService);

  // 4. derived state
  protected readonly displayName = computed(() => this.user().name.toUpperCase());

  // 5. methods
  open() { this.opened.emit(); }
}

Clean templates: logic does not belong in HTML

Use the built-in control flow. @if / @for / @switch replaced ngIf and ngFor, they read like JavaScript, and they compile to faster instructions. @for requires track, which is the point: it forces you to give the diff a key instead of re-rendering the list. Precompute in computed() instead of calling methods from the template (a method runs on every check), prefer [class.x] and [style.x] over ngClass and ngStyle, and use @let once at the top of a block instead of repeating (x$ | async).

✗ then
<div *ngIf="user">
  <li *ngFor="let i of items; trackBy: byId">
    {{ compute(i) }}
  </li>
</div>
✓ now
@if (user()) {
  @for (i of items(); track i.id) {
    {{ i.label }}
  }
}

And lazy-load heavy parts of the view itself, not just routes, with @defer. It is the built-in way to code-split a template: load on viewport, on interaction, or on a condition, with placeholder and loading blocks for free.

dashboard.component.html
@defer (on viewport) {
  <heavy-chart [data]="data()" />
} @placeholder {
  <skeleton-chart />
} @loading (minimum 200ms) {
  <spinner />
}

Signals and zoneless: the part that actually changed

This is the section that makes the old manifesto obsolete. Signals are the state model now. signal() for local state, computed() for derived values, effect() for side effects (always clean up), linkedSignal() for derived state you can also reset, toSignal() to bridge a legacy Observable. Treat signal values as immutable and use set() or update(), there is no mutate() on purpose.

counter.component.ts
readonly count = signal(0);
readonly doubled = computed(() => this.count() * 2);

increment() {
  this.count.update(c => c + 1);  // not mutate()
}
Note · zoneless and OnPush, per the Angular team As of v21, zoneless change detection is the default and zone.js is gone (~33KB lighter). As of v22, OnPush is the default strategy. They work together, not against each other: reading a signal in a template auto-marks the component, so you keep OnPush and you stop calling markForCheck() by hand. Do not "turn OnPush off." It is the recommended default now, and signals make it effortless.

In a zoneless app, change detection runs when a signal changes, when an event fires, or when the async pipe emits, not on every stray setTimeout. The result is cleaner stack traces, smaller bundles, and a rendering model you can actually reason about. The reflex to scatter ChangeDetectionStrategy.OnPush across the app is over; OnPush is just the default, and signals do the notifying.

Data: httpResource for reads, RxJS for the rest

Fetching data used to mean an HttpClient call, a manual subscribe, and an unsubscribe you would forget half the time. For reads, that is gone. httpResource(), resource(), and rxResource() (stable in v22) give you the value, loading state, and error as signals, with cancellation and automatic re-fetch when a dependency changes, and nothing to tear down.

✗ then
user!: User;
ngOnInit() {
  this.http.get<User>(url)
    .subscribe(u => this.user = u);
  // + remember to unsubscribe
}
✓ now
readonly user = httpResource<User>(
  () => `/api/users/${this.id()}`
);
// user.value(), user.isLoading(), user.error()

The line the Angular team draws, and so do we: httpResource is for reads. Do not use it for mutations. POST, PUT, and DELETE stay on HttpClient with an Observable. And RxJS did not die, it got a smaller, sharper job: real event streams and coordination. Debounced search, websockets, merging several sources. For those the old rules hold, and the worst one is still nested subscriptions.

✗ nested subscribe
this.id$.subscribe(id => {
  this.api.getUser(id).subscribe(u => {
    this.user = u;
  });
});
✓ switchMap
readonly user = this.id$.pipe(
  switchMap(id => this.api.getUser(id)),
  takeUntilDestroyed(),
);

Beyond that: use the async pipe or toSignal() instead of subscribing in the template, keep subscriptions in ngOnInit not constructors, never subscribe inside a per-component (non-singleton) service, and clean up with takeUntilDestroyed().

Performance and SSR

Most performance work is structural now, not micro-tuning. @defer (stable since v18) splits the view. Keep pipes pure so Angular caches them, and wrap any template function call in a pure pipe instead of running it on every check. For public pages, server-render: use Angular SSR, set Open Graph and Title tags from the route, and turn on incremental hydration (v19) so the page hydrates on interaction or viewport instead of all at once, which kills the time a page sits there looking ready but doing nothing.

post-detail.component.ts
private meta = inject(Meta);
private title = inject(Title);

ngOnInit() {
  this.title.setTitle(post.title);
  this.meta.updateTag({ property: 'og:title', content: post.title });
  this.meta.updateTag({ property: 'og:image', content: post.image });
}

Quality, and what we keep from the old days

Strict TypeScript, no any, explicit return types, small pure functions, ESLint and Prettier doing the boring arguments for you. Violations break the build.

tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true
  }
}

Tests run on Vitest now, which became the default runner in v21 in place of Karma. For forms, Signal Forms is the emerging API (experimental in v21); reactive forms are still fine. And for state, a service exposing a few signals replaces a global store more often than people expect. Reach for NGXS when you genuinely need centralized, plugin-driven state across many features, not by reflex.

Dry observations from the trenches

The migration is not a rewrite. Standalone components and NgModules run side by side, so you convert leaf components first and never schedule a big-bang. The teams that called it a rewrite were usually the ones who waited three years to start.

Most "Angular is slow" reports trace back to a function called from a template tens of thousands of times a second. It was almost never the framework. Move the work into a computed() and the problem quietly disappears.

ng update does more than people expect. The schematics rewrote our control flow and our inject() calls across a large app in an afternoon, the kind of change teams budget weeks for. Running it the day each version drops is cheaper than skipping four releases and migrating under pressure later.

Signals did not kill RxJS. They killed the bad RxJS, the five-operator pipe you wrote to fetch a single number. What is left is RxJS doing the job it was always good at, coordinating real streams over time.

And if a new developer cannot guess from the file name where the code lives, the structure is wrong, not the developer. That is the entire reason the naming rules stay boring.

how angular got here · tap a version

What AI coding agents get wrong about Angular

Generative tools learned Angular from years of public code, and most of that code is old. Ask an agent for a component and you will usually get @NgModule, @Input() decorators, *ngFor, constructor injection, and a manual subscribe, because that is what the training data is full of. It compiles, it works in a demo, and it is three years behind. The manifesto is the correction layer: standalone, signal inputs, @if / @for, inject(), httpResource for reads. We hand the agent the rules, then we review against them, because the agent will confidently write the version of Angular the framework spent three years removing. That gap, a tool producing plausible but stale code, is the same one we wrote about on the hiring side in AI-native vs AI-powered developers.

Drop this into your AI coding agent

The fix for the training-data problem is to hand the agent the rules up front. Paste this into your CLAUDE.md, your Cursor rules, or the top of a prompt, and the Angular it generates comes out current instead of three years stale. The copy button is right there.

angular-2026.rules.md
Write Angular the 2026 way. Non-negotiable house rules:

- Standalone components only. Never NgModule on new code.
- Signal inputs/outputs: input(), input.required<T>(), output(), model().
  Never @Input() or @Output().
- inject(), not constructor injection.
- Control flow: @if / @for (track is required) / @switch.
  Never *ngIf or *ngFor.
- State: signal() / computed() / effect(). Values immutable;
  set()/update(), never mutate().
- Change detection: zoneless + OnPush (the default).
  Never call markForCheck() when using signals.
- Reads: httpResource() / resource(). Mutations (POST/PUT/DELETE):
  HttpClient + RxJS. Never httpResource for mutations.
- Templates: async pipe or toSignal(); takeUntilDestroyed()
  for any manual subscription.
- Lazy-load heavy view chunks with @defer.
  SSR + incremental hydration for public pages.
- Strict TypeScript, no any. Tests on Vitest.

Where Angular sits in 2026, and why we keep this current

Angular in 2026 is a different framework than the one that earned its reputation, good or bad. It is signal-reactive, standalone, zoneless, server-rendered when it should be, and smaller than it has been in years. The teams that fell behind are still shipping the 2021 dialect and calling it Angular, and the gap between "knows Angular" and "knows current Angular" is now wide enough to lose a contract over. The whole point of a manifesto is that it is not frozen. We update this every time the framework moves, because it moves about every six months and it is not slowing down. You can still read the 2019 edition of this manifesto, or the same era's best-practices PDF; the distance between those and this is how far the framework has moved since.

If you are building something serious on Angular and want it written the way the framework actually wants in 2026, that is the work we do.

DM
Dmitriy Melnichenko
Founder and CTO, 2muchcoffee
We have shipped Angular in production since the AngularJS days and keep this manifesto current as the framework moves.