Skip to main content

10 - Migrating from Angular 17 to Angular 21: A Practical Guide

Step-by-step guide to migrate from Angular 17 to 21: ng update, breaking changes per version, library compatibility, testing, and final checklist.

What do you think about this article?

At a Glance

Step-by-step guide to migrate from Angular 17 to 21: ng update, breaking changes per version, library compatibility, testing, and final checklist.

Migrating from Angular 17 to Angular 21: A Practical Guide

Migrating an Angular application across four major versions might seem like a daunting task, but the Angular team has invested heavily in backward compatibility and automatic migration tooling. Each version from 17 to 21 has introduced significant improvements: from modern control flow to stable Signals, from zoneless change detection to TypeScript 5.9 support.

In this tenth and final article of the Modern Angular series, we will tackle the migration step by step, covering every transition from Angular 17 all the way to version 21. This is not just about updating dependencies: each version brings new APIs, deprecations, and best practices that are essential to understand in order to fully leverage the framework.

What You Will Learn in This Article

  • The roadmap of features introduced in Angular 18, 19, 20, and 21
  • How to prepare your project before starting the migration
  • Step-by-step procedure for each version jump (17→18→19→20→21)
  • ng update commands and handling breaking changes
  • Updating third-party dependencies (Angular Material, NgRx, etc.)
  • How to run tests after each migration phase
  • A complete checklist for validating the migration

Modern Angular Series Overview

# Article Focus
1Angular SignalsFine-grained reactivity
2Zoneless Change DetectionRemoving Zone.js
3New Templates @if, @for, @deferModern control flow
4Standalone ComponentsNgModule-free architecture
5Signal FormsReactive forms with Signals
6SSR and Incremental HydrationServer-Side Rendering
7Core Web Vitals in AngularPerformance and metrics
8Angular PWAProgressive Web App
9Advanced Dependency InjectionTree-shakable DI
10You are here → Migrating from Angular 17 to 21Migration guide

1. Version Overview: From Angular 17 to 21

Before proceeding with the migration, it is essential to understand the key features introduced in each version. Angular follows a six-month release cycle: each major version brings new features, stabilization of experimental APIs, and occasional breaking changes.

Release Timeline and Key Features

Version Release TypeScript Key Features
Angular 17 Nov 2023 5.2 - 5.3 Control flow (@if, @for, @switch), @defer, Signals developer preview, new branding
Angular 18 May 2024 5.4 - 5.5 Zoneless developer preview, stable signal inputs/outputs, stable deferrable views
Angular 19 Nov 2024 5.6 - 5.7 Standalone default, incremental hydration, linkedSignal, resource API
Angular 20 May 2025 5.8 Stable zoneless, signal forms preview, improved effect(), authoring format
Angular 21 Nov 2025 5.9 Zoneless default for new projects, TypeScript 5.9, runtime performance

As you can see, the common thread across the last four versions is clear: Signals as the central reactive primitive, removal of Zone.js, and architecture simplification through standalone components. Each migration builds on the foundations of the previous one.

2. Preparing for Migration

A successful migration begins well before the first ng update. The preparation phase is crucial to avoid unexpected issues and reduce downtime. Here are the fundamental steps.

Checking Compatibility

First, check the current project version and installed dependencies. Each Angular version requires a minimum Node.js and TypeScript version.

System Requirements by Version

Angular Min Node.js Recommended Node.js TypeScript RxJS
17.x 18.13 20.x 5.2 - 5.3 7.x
18.x 18.19 20.x / 22.x 5.4 - 5.5 7.x
19.x 18.19 22.x 5.6 - 5.7 7.x
20.x 20.x 22.x 5.8 7.x
21.x 20.x 22.x 5.9 7.x

Pre-Migration Checklist

Preliminary checks

# 1. Check the current Angular version
ng version

# 2. Check Node.js version
node --version

# 3. Ensure the repository is clean
git status

# 4. Create a dedicated migration branch
git checkout -b feat/angular-21-migration

# 5. Run the current test suite for a baseline
npm test -- --watch=false

# 6. Production build for baseline verification
npm run build

# 7. Check for outdated dependencies
npm outdated

# 8. Check for vulnerabilities
npm audit
    

Golden Rule of Angular Migration

Never skip versions. Angular only supports migration one major version at a time. If you are on Angular 17, you must go through 18, then 19, then 20, and finally 21. Each step includes automatic schematics that update your code, and these are designed to work only between consecutive versions.

3. Angular 17 → 18: Control Flow and Signal Inputs

The first jump is often the most significant in terms of code refactoring. Angular 18 stabilized the modern control flow (@if, @for, @switch) introduced as a developer preview in Angular 17, and brought signal inputs and signal outputs to stability.

Update Commands

ng update for Angular 17 → 18

# Update Angular CLI and Core
ng update @angular/core@18 @angular/cli@18

# If using Angular Material
ng update @angular/material@18

# If using Angular CDK
ng update @angular/cdk@18

# Resolve any peer dependency conflicts
npm install --legacy-peer-deps
    

Automatic Control Flow Migration

Angular 18 includes a schematic that automatically converts structural directives (*ngIf, *ngFor, *ngSwitch) to the new block syntax.

Running the automatic control flow migration

# Automatic control flow migration
ng generate @angular/core:control-flow

# Or, for a specific project in a workspace
ng generate @angular/core:control-flow --project=my-app

# After migration, review the changes
git diff
    

Before and After: Control Flow

Before (Angular 17 - structural directives)

<!-- *ngIf with else -->
<div *ngIf="user; else noUser">
  Welcome, {{ user.name }}
</div>
<ng-template #noUser>
  <p>Please log in</p>
</ng-template>

<!-- *ngFor with trackBy -->
<ul>
  <li *ngFor="let item of items; trackBy: trackById">
    {{ item.name }}
  </li>
</ul>

<!-- *ngSwitch -->
<div [ngSwitch]="status">
  <p *ngSwitchCase="'active'">Active</p>
  <p *ngSwitchCase="'inactive'">Inactive</p>
  <p *ngSwitchDefault>Unknown</p>
</div>
    
After (Angular 18 - modern control flow)

<!-- @if with @else -->
@if (user) {
  <div>Welcome, {{ user.name }}</div>
} @else {
  <p>Please log in</p>
}

<!-- @for with track -->
<ul>
  @for (item of items; track item.id) {
    <li>{{ item.name }}</li>
  } @empty {
    <li>No items found</li>
  }
</ul>

<!-- @switch -->
@switch (status) {
  @case ('active') { <p>Active</p> }
  @case ('inactive') { <p>Inactive</p> }
  @default { <p>Unknown</p> }
}
    

Signal Inputs and Outputs

Migrating from @Input/@Output to signal inputs/outputs

// BEFORE (Angular 17) - classic decorators
@Component({ selector: 'app-user-card' })
export class UserCardComponent {
  @Input() name: string = '';
  @Input() age: number = 0;
  @Output() selected = new EventEmitter<string>();

  onClick(): void {
    this.selected.emit(this.name);
  }
}

// AFTER (Angular 18) - signal inputs/outputs
import { input, output } from '@angular/core';

@Component({ selector: 'app-user-card' })
export class UserCardComponent {
  name = input<string>('');
  age = input<number>(0);
  selected = output<string>();

  onClick(): void {
    this.selected.emit(this.name());
  }
}
    

Key Features of Angular 18

  • Stable control flow: @if, @for, @switch and @defer fully stable
  • Signal inputs/outputs: Stable input() and output() APIs
  • Zoneless developer preview: First support for applications without Zone.js
  • Fallback content for ng-content: Default content for content projection
  • Route redirects with functions: Dynamic redirects based on logic
  • Firebase App Hosting: New optimized hosting service for Angular

4. Angular 18 → 19: Standalone Default and Hydration

Angular 19 represents an important architectural shift: standalone components become the default. The standalone: true flag is no longer necessary because all components are standalone by default. Additionally, incremental hydration reaches stability, and new reactive primitives like linkedSignal and the resource API make their appearance.

Update Commands

ng update for Angular 18 → 19

# Update Angular CLI and Core
ng update @angular/core@19 @angular/cli@19

# If using Angular Material
ng update @angular/material@19

# Check and fix deprecation warnings
ng build 2>&1 | grep -i "deprecated"
    

Standalone by Default

The Angular 19 schematic automatically removes standalone: true from all components, since it becomes the default behavior. If a component is NOT standalone, it must now explicitly declare standalone: false.

Before and after: standalone default

// BEFORE (Angular 18) - explicit standalone
@Component({
  standalone: true,      // Explicitly required
  selector: 'app-header',
  imports: [RouterLink, NgOptimizedImage],
  templateUrl: './header.component.html',
})
export class HeaderComponent {}

// AFTER (Angular 19) - standalone by default
@Component({
  // standalone: true is implicit, removed by schematic
  selector: 'app-header',
  imports: [RouterLink, NgOptimizedImage],
  templateUrl: './header.component.html',
})
export class HeaderComponent {}
    

linkedSignal and resource API

New reactive APIs in Angular 19

import { signal, linkedSignal, resource } from '@angular/core';

@Component({ selector: 'app-product-list' })
export class ProductListComponent {
  // linkedSignal: derived signal with override capability
  selectedCategory = signal('electronics');
  currentPage = linkedSignal(() => {
    // Reset to page 1 when category changes
    this.selectedCategory();
    return 1;
  });

  // resource API: reactive async data loading
  products = resource({
    request: () => ({
      category: this.selectedCategory(),
      page: this.currentPage()
    }),
    loader: async ({ request }) => {
      const response = await fetch(
        `/api/products?cat=${request.category}&page=${request.page}`
      );
      return response.json();
    }
  });

  changePage(page: number): void {
    this.currentPage.set(page);
  }
}
    

Key Features of Angular 19

  • Standalone default: All components are standalone by default
  • Stable incremental hydration: Progressive hydration based on triggers (viewport, hover, timer)
  • linkedSignal: Derived signal with overridable value (developer preview)
  • resource API: Async data loading integrated with Signals
  • Strict standalone enforcement: Build error for missing imports
  • SSR improvements: Event replay for interactions during hydration

5. Angular 19 → 20: Stable Zoneless and Signal Forms

Angular 20 marks a historic milestone: zoneless change detection becomes stable. After nearly two years of developer preview, Angular applications can officially remove Zone.js with full stability guarantees. Additionally, signal forms make their appearance as a developer preview, proposing a new way to handle forms entirely based on Signals.

Update Commands

ng update for Angular 19 → 20

# Update Angular CLI and Core
ng update @angular/core@20 @angular/cli@20

# If using Angular Material
ng update @angular/material@20

# After the update, start the dev server
ng serve
    

Adopting Zoneless Change Detection

With Angular 20, you can safely remove Zone.js. The schematic does not do it automatically to avoid breakage, but this is the right time to adopt zoneless if you have not already.

Enabling zoneless change detection

// app.config.ts - Enable zoneless
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideClientHydration } from '@angular/platform-browser';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    // Replaces provideZoneChangeDetection()
    provideZonelessChangeDetection(),
    provideRouter(routes),
    provideClientHydration(),
  ]
};
    
Removing Zone.js from the project

# 1. Remove zone.js from dependencies
npm uninstall zone.js

# 2. Remove the polyfill from angular.json
# Find and remove "zone.js" from the "polyfills" array

# 3. Verify there are no direct imports
grep -r "zone.js" src/
    

Warning: Code Incompatible with Zoneless

Some common patterns stop working without Zone.js because they relied on monkey-patching of async APIs. Search for and fix these patterns in your code:

  • Direct component property changes after setTimeout or setInterval: use signal() instead
  • Using Promise.then() to update UI state: convert to resource() or use signal.set()
  • Third-party libraries that modify state outside Angular: use inject(ChangeDetectorRef) as a temporary workaround
  • Tests with fakeAsync/tick: replace with await fixture.whenStable()

Key Features of Angular 20

  • Stable zoneless change detection: provideZonelessChangeDetection() fully stable
  • Signal forms (developer preview): New signal-based form APIs
  • Improved effect(): Optional synchronous execution, better lifecycle integration
  • Stable linkedSignal: Derived signal with override now fully stable
  • Experimental authoring format: New format for defining components (.ng files)
  • Deprecated API removal: Cleanup of APIs deprecated since Angular 16-17

6. Angular 20 → 21: TypeScript 5.9 and Zoneless by Default

Angular 21 is the version that consolidates all the work from previous releases. New projects generated with ng new are zoneless by default, TypeScript 5.9 brings improvements to type checking and developer experience, and the runtime receives further performance optimizations.

Update Commands

ng update for Angular 20 → 21

# Update Angular CLI and Core
ng update @angular/core@21 @angular/cli@21

# If using Angular Material
ng update @angular/material@21

# Update TypeScript to 5.9
npm install typescript@5.9 --save-dev

# Verify the build
ng build
    

TypeScript 5.9: Relevant Features for Angular

TypeScript 5.9 features useful in Angular

// 1. Improved type narrowing in templates
// TypeScript 5.9 improves narrowing support
// in @if blocks of Angular templates

// 2. Stable import attributes
import data from './config.json' with { type: 'json' };

// 3. Compiler performance improvements
// Faster builds thanks to improved caching
// and reduced memory allocations

// 4. Improved utility types for Signals
import { Signal, WritableSignal } from '@angular/core';

// NoInfer<T> prevents unwanted inference
function createState<T>(
  initial: T,
  validator: (value: NoInfer<T>) => boolean
): WritableSignal<T> {
  const state = signal(initial);
  // ...validation logic
  return state;
}
    

Key Features of Angular 21

  • Zoneless by default: ng new generates projects without Zone.js
  • TypeScript 5.9: Full support with improved type checking
  • Runtime performance: Optimizations to change detection and rendering
  • Improved signal forms: More mature and complete APIs
  • Stable resource API: Async data loading fully stable
  • NgModule-based code removal: Further deprecation of module-based APIs

7. ng update Step by Step: The Complete Procedure

Let us now look at the complete procedure for migrating from version 17 to 21, including handling breaking changes and dependency conflicts. Each step must be completed and verified before proceeding to the next.

Complete migration script 17 → 21

#!/bin/bash
# Angular 17 -> 21 Migration Script

echo "=== STEP 1: Angular 17 -> 18 ==="
ng update @angular/core@18 @angular/cli@18
npm install
ng build
npm test -- --watch=false
git add -A && git commit -m "chore: migrate to Angular 18"

echo "=== STEP 2: Angular 18 -> 19 ==="
ng update @angular/core@19 @angular/cli@19
npm install
ng build
npm test -- --watch=false
git add -A && git commit -m "chore: migrate to Angular 19"

echo "=== STEP 3: Angular 19 -> 20 ==="
ng update @angular/core@20 @angular/cli@20
npm install
ng build
npm test -- --watch=false
git add -A && git commit -m "chore: migrate to Angular 20"

echo "=== STEP 4: Angular 20 -> 21 ==="
ng update @angular/core@21 @angular/cli@21
npm install
ng build
npm test -- --watch=false
git add -A && git commit -m "chore: migrate to Angular 21"

echo "=== Migration complete! ==="
ng version
    

Handling Breaking Changes

When ng update encounters a breaking change, the process stops with an error message. Here are the strategies for resolving the most common issues.

Common Breaking Changes and Solutions

Version Breaking Change Solution
17→18 Removal of residual ViewEngine APIs Replace with Ivy equivalents
18→19 standalone: true becomes default Add standalone: false to non-standalone components
18→19 Removal of legacy @angular/http Use @angular/common/http
19→20 Deprecation of Observable-based patterns for some internal services Migrate to signal-based equivalents
20→21 TypeScript 5.9 required Update TypeScript and fix type errors
20→21 Removal of APIs deprecated since v17 Consult the Angular deprecation guide

Resolving Peer Dependency Conflicts

Strategies for resolving dependency conflicts

# If ng update fails due to peer dependencies
# Strategy 1: Force the update
ng update @angular/core@21 @angular/cli@21 --force

# Strategy 2: Manually update conflicting dependencies
npm install @angular/core@21 @angular/cli@21 --legacy-peer-deps
npm install

# Strategy 3: Use --allow-dirty if you have uncommitted changes
ng update @angular/core@21 --allow-dirty

# Strategy 4: Clean install from scratch
rm -rf node_modules package-lock.json
npm install

# Verify dependency status
npm ls @angular/core
    

8. Third-Party Dependency Migration

Third-party dependencies are often the main bottleneck in a migration. Not all libraries immediately support the new Angular version, and some require substantial updates. Here is a guide for the most common libraries.

Main Library Compatibility Matrix

Library Angular 18 Angular 19 Angular 20 Angular 21
@angular/material 18.x 19.x 20.x 21.x
@ngrx/store 18.x 19.x 20.x 21.x
@angular/fire 18.x 19.x 18.x+ 19.x+
ng-bootstrap 16.x 17.x 18.x 19.x
ngx-translate 15.x 16.x 16.x+ 16.x+
primeng 17.x 18.x 19.x 19.x+
@ng-icons 27.x 28.x 29.x 30.x+

Dependency Strategy

Checking and updating dependencies

# 1. List all Angular-related dependencies
npm ls | grep angular

# 2. Check available versions
npm view @angular/material versions --json | tail -20
npm view @ngrx/store versions --json | tail -20

# 3. Update primary dependencies
ng update @angular/material@21
ng update @ngrx/store@21

# 4. For libraries without ng update schematics
npm install ngx-translate@latest
npm install primeng@latest

# 5. Verify compatibility after update
ng build
npm test -- --watch=false
    

Dependencies Not Yet Updated

If a third-party library does not yet support the target Angular version, you have these options:

  • Wait: Monitor the library releases and defer the migration for that dependency
  • Temporary fork: If necessary, create a fork with the minimum required changes
  • --legacy-peer-deps: Ignore the conflict if the library works anyway (verify with tests)
  • Replace: Evaluate compatible alternatives. For example, from ngx-translate to Angular native i18n
  • Contribute: Open a PR on the library to add support for the new version

9. Testing After Migration

After each step of the migration, it is essential to verify that the application works correctly. Automated tests are your safety net, but the migration itself can break existing tests. Here is how to approach this phase.

Running the Test Suite

Post-migration testing procedure

# 1. Run all unit tests
npm test -- --watch=false --browsers=ChromeHeadless

# 2. Run e2e tests (if present)
npm run e2e

# 3. Production build (catches AOT errors)
ng build --configuration=production

# 4. Start the SSR server and verify manually
npm run serve:ssr:portfolio

# 5. Check for deprecation warnings
ng build 2>&1 | grep -i "warning\|deprecated"

# 6. Verify performance with Lighthouse
npx lighthouse http://localhost:4200 --output=json --output-path=./lighthouse-report.json
    

Common Fixes for Broken Tests

Updating tests for Angular 21

// BEFORE: TestBed with NgModule-style
beforeEach(async () => {
  await TestBed.configureTestingModule({
    declarations: [MyComponent],   // deprecated with standalone
    imports: [HttpClientTestingModule],
    providers: [MyService]
  }).compileComponents();
});

// AFTER: TestBed with standalone components
beforeEach(async () => {
  await TestBed.configureTestingModule({
    imports: [MyComponent],   // standalone component goes in imports
    providers: [
      provideHttpClientTesting(),  // new provider-based testing
      MyService
    ]
  }).compileComponents();
});

// BEFORE: fakeAsync with Zone.js
it('should load data', fakeAsync(() => {
  component.loadData();
  tick(1000);
  expect(component.data).toBeTruthy();
}));

// AFTER: async/await without Zone.js
it('should load data', async () => {
  component.loadData();
  await fixture.whenStable();
  fixture.detectChanges();
  expect(component.data()).toBeTruthy();  // signal()
});
    

Common Test Errors After Migration

Error Cause Solution
NullInjectorError: No provider for HttpClient HttpClientTestingModule deprecated Use provideHttpClientTesting()
Component is not a known element Standalone component not imported Add the component to imports in TestBed
Cannot read property of signal Signal not invoked as a function Change component.value to component.value()
fakeAsync timer not cleared Zone.js removed Convert to async/await
Expression has changed Zoneless change detection Use fixture.detectChanges() after every change

10. Final Migration Checklist

To make sure the migration is complete and nothing has been missed, follow this detailed checklist divided into three phases: pre-migration, during migration, and post-migration.

Phase 1: Pre-Migration

  • Clean repository: no uncommitted changes
  • Dedicated branch created: feat/angular-21-migration
  • Green test suite: all tests pass on the current version
  • Working production build: ng build completed without errors
  • Node.js updated to the minimum required (20.x for Angular 20/21)
  • Backup of package.json and package-lock.json
  • Documentation of third-party dependencies and their versions
  • Review of breaking changes for each target version on update.angular.io

Phase 2: During Migration

  • Migrate one version at a time: 17→18→19→20→21
  • For each step: ng update, npm install, ng build, npm test
  • Commit after each successful migration
  • Run automatic migration schematics (control flow, standalone, etc.)
  • Resolve every deprecation warning before proceeding to the next step
  • Update Angular Material and other Angular dependencies to the same major version
  • Verify AOT compilation: ng build --configuration=production
  • Check the browser console for runtime errors

Phase 3: Post-Migration

  • All tests (unit + e2e) pass
  • Production build completed without errors or warnings
  • SSR working: verify with npm run serve:ssr
  • Performance verified: Lighthouse score ≥ 90 across all categories
  • Zero deprecation warnings in the build log
  • All third-party dependencies compatible and updated
  • Zoneless change detection adopted (recommended)
  • Modern control flow (@if, @for) replacing structural directives
  • Signal inputs/outputs replacing @Input/@Output decorators
  • Tests updated for new APIs (provideHttpClientTesting, etc.)
  • package-lock.json committed and clean
  • Deploy to staging environment for manual testing before production

Summary and Series Conclusion

Migrating from Angular 17 to 21 is not just a version update: it is an evolution of your entire application architecture. Moving from the old model based on Zone.js and NgModules to the new Signals-first, standalone-by-default paradigm represents a substantial improvement in performance, maintainability, and developer experience.

The key to success is graduality: migrate one version at a time, test at every step, adopt new APIs progressively. Angular is designed to make this process as painless as possible thanks to automatic schematics and excellent backward compatibility.

Key Concepts of This Article

  • Gradual migration: Always one major version at a time (17→18→19→20→21)
  • ng update: The primary tool that updates dependencies and applies automatic schematics
  • Modern control flow: @if, @for, @switch replace *ngIf, *ngFor, *ngSwitch
  • Signal inputs/outputs: input() and output() replace @Input and @Output
  • Standalone default: From Angular 19 all components are standalone by default
  • Stable zoneless: From Angular 20 you can safely remove Zone.js
  • TypeScript 5.9: Angular 21 requires TypeScript 5.9 for new features
  • Testing: Update tests from NgModule-style to standalone and from fakeAsync to async/await
  • Dependencies: Update Angular Material, NgRx and third-party libraries to the same major

Complete Modern Angular Series Summary

# Article Key Concept
1Angular SignalsFine-grained reactivity with signal(), computed(), effect()
2Zoneless Change DetectionRemoving Zone.js for superior performance
3Templates @if, @for, @deferModern control flow and declarative lazy loading
4Standalone ComponentsNgModule-free, tree-shakable architecture
5Signal FormsReactive forms based on Signals
6SSR and Incremental HydrationServer-Side Rendering with progressive hydration
7Core Web VitalsOptimizing LCP, INP, CLS for Google ranking
8Angular PWAProgressive Web App with Service Worker
9Advanced Dependency InjectionTree-shakable DI, injection token, multi-provider
10Migrating from Angular 17 to 21Complete step-by-step migration guide

With this article, the Modern Angular series comes to a close. We have covered all the fundamental aspects of the framework, from reactive primitives (Signals) to state management (Signal Forms), from rendering strategies (SSR, hydration) to performance optimization (Core Web Vitals, PWA), from architecture (standalone, advanced DI) to practical migration. We hope this series has been useful for mastering Angular in its most modern and performant incarnation.

Frequently asked questions

What are the key features introduced in each version of Angular from 17 to 21?

The common thread across the last four versions is clear: Signals as the central reactive primitive, removal of Zone.js, and architecture simplification through standalone components.

Why should you never skip versions when migrating an Angular application?

Angular only supports migration one major version at a time. If you are on Angular 17, you must go through 18, then 19, then 20, and finally 21.

What is the purpose of automatic schematics in each step of the migration process?

These are designed to work only between consecutive versions and update your code automatically.

Why is testing after migration essential?

The migration itself can break existing tests, so it's crucial to verify that the application works correctly after each step.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects