Faire une migration de Angular 4 à Angular 21 (… oui c’est possible)

8–12 minutes
Monitor displaying burning Angular 4 text amid Angular 21 notes

Faire une migration de Angular 4 à Angular 21 (… oui c’est possible)

Contexte

Récemment on m’a proposé un nouveau projet un peu fou, migrer une application web de Angular 4.3 à du Angular 21.

L’application existait depuis plusieurs années, elle avait été développée avec une version ancienne d’Angular et avait évolué au fil du temps sans forcément suivre toutes les évolutions du Framework.

C’est assez classique, on commence avec une application propre, puis on ajoute une fonctionnalité, puis une autre, puis une dépendance… Et à la fin on se retrouve avec une montagne de fichiers et de lignes de code legacy que plus personne n’ose toucher « tant que ca fonctionne ».

Le but était simple :

  • mettre à jour la version de Angular,
  • profiter de la migration pour remettre un peu d’ordre dans le code,
  • et tout ca en moins de 6 mois et toute seule…

Et c’est là que le projet est devenu intéressant.

La théorie

Angular a énormément évolué depuis Angular 4.

Et forcément, quand on saute autant de versions, on récupère plusieurs années d’évolutions du framework, de TypeScript, de Node.js, de dépendances et d’infrastructure .

Aujourd’hui, Angular propose notamment les composants standalone, les nouvelles syntaxes de templates, les APIs basées sur les signals, un nouveau système de build et une approche différente du testing… en résumé, il faut tout repenser.

Pourquoi ne pas faire 4 → 5 → 6 → 7… ?

C’est probablement la première question qui vient à l’esprit.

Pourquoi ne pas migrer progressivement chaque version ?

C’est une vraie possibilité, mais dans notre contexte, cela aurait représenté énormément d’étapes intermédiaires et surtout beaucoup de temps passé à maintenir temporairement une application dans des états intermédiaires.

L’objectif était donc différent : partir de l’existant et arriver à une application Angular 21 moderne, maintenable et testable.

Angular fournit aujourd’hui des outils de migration pour accompagner certaines transformations et recommande de vérifier le projet après chaque étape. : Update guide • Angular.

Mais dans notre cas, nous avons choisi d’organiser le travail autour d’une feuille de route spécifique à notre projet.

Notre feuille de route

Avant de toucher au code, nous avons découpé la migration en plusieurs grandes étapes.

Angular 4.3
│
├── Infrastructure et configuration
│
├── Dépendances
│
├── Composants
│
├── Services
│
├── Interfaces
│
├── Utils
│
├── Templates
│
├── TypeScript
│
├── Stratégie de tests
│
└── Nettoyage
│
Angular 21

L’idée était simple : ne pas tout casser en même temps... Même si, soyons honnêtes, parfois on avait quand même un peu l’impression de tout casser en même temps.

La pratique

Préparer l’infrastructure

Avant même de parler de composants Angular, il fallait déjà s’occuper de l’environnement d’exécution. Parce qu’une vieille application Angular tourne rarement avec les versions modernes de Node.js, npm et TypeScript.

Et Angular 21 a ses propres contraintes de compatibilité. Par exemple, Angular 21 supporte actuellement Node.js 20.19+, 22.12+ ou 24+, ainsi que TypeScript 5.9.x.

Notre première étape a donc été de vérifier :

  • la version de Node.js ;
  • la version de npm ;
  • la version de TypeScript ;
  • la version d’Angular CLI ;
  • la version du système d’exploitation utilisée dans nos builds ;
  • la configuration Docker ;
  • les outils utilisés dans la CI/CD.

Parce qu’il ne sert à rien de migrer Angular si le pipeline est incapable de construire l’application.

Et forcément… Notre vieille image Docker n’était plus vraiment adaptée. Une image basée sur une ancienne version de Node.js ne va pas miraculeusement devenir compatible avec Angular 21 parce qu’on lui demande gentiment… Même si on le souhaite très fort.

Il a donc fallu revoir le Dockerfile. L’objectif n’était pas simplement de changer le numéro de version. Il fallait vérifier tout ce qui dépendait de l’environnement :

  • installation npm ;
  • commandes de build ;
  • scripts shell ;
  • certificats ;
  • variables d’environnement ;
  • outils utilisés dans la CI ;
  • génération des artefacts ;
  • serveur utilisé pour exposer l’application.

Adapter la configuration du projet

Ensuite, direction angular.json, et là… On retrouve parfois des choses qu’on avait complètement oubliées.

Les projets Angular ont énormément évolué, à partir d’Angular 17, Angular propose un nouveau système de build basé notamment sur esbuild et Vite, avec un build plus moderne et plus rapide.

Il faut donc vérifier notamment :

  • les builders ;
  • les configurations build ;
  • les configurations test ;
  • les assets ;
  • les styles ;
  • les scripts ;
  • les polyfills ;
  • les environnements ;
  • les configurations de production ;
  • les options devenues obsolètes.

Et dans une migration aussi importante, il faut accepter une chose : certaines configurations historiques n’ont plus de raison d’exister. Le but n’est pas de conserver absolument chaque ligne de angular.json. Le but est d’arriver à une configuration moderne et compréhensible.

Mettre à jour les dépendances

C’est probablement une des parties les plus chronophages. Parce qu’une application Angular n’utilise jamais uniquement Angular.

On retrouve généralement :

Angular
├── Angular Material
├── RxJS
├── librairie de composants
├── librairie de formulaires
├── librairie de graphiques
├── outils de test
├── outils de lint
├── outils de build
└── plein d'autres choses...

Et certaines dépendances anciennes ne sont tout simplement plus compatibles avec Angular 21, il faut donc faire le tri. Des questions se posent pour chaque dépendance :

  • Est-ce qu’elle existe encore ?
  • Est-elle compatible ?
  • Est-elle encore utile ?
  • Existe-t-il une alternative moderne ?

Et surtout : est-ce qu’on en a vraiment besoin ?
Parce qu’une migration est aussi une excellente occasion de supprimer les dépendances inutiles.

Migrer les composants

Les composants Angular ont énormément évolué. Alors oui, tout a énormément évolué depuis Angular 4 on est d’accords mais les composants sont les éléments de base du framework.

Aujourd’hui, les composants Angular sont standalone par défaut et déclarent directement leurs dépendances.

On passe donc progressivement d’une architecture comme :

@NgModule({
declarations: [
UserComponent
],
imports: [
CommonModule
]
})
export class UserModule {}

à quelque chose comme :

@Component({
selector: 'app-user',
imports: [
CommonModule
],
templateUrl: './user.component.html'
})
export class UserComponent {}

Attention : le passage aux standalone n’est pas juste un changement de syntaxe.

Il faut comprendre les dépendances de chaque composant. Les changements apportés par les dernières versions de Angular pourront faire l’objet d’un autre article si ca vous intéresse.

Factoriser les services

Une migration est également l’occasion de regarder les services.
Et parfois… ça pique.

La migration permet de poser une question simple : est-ce que ce service fait vraiment une seule chose ?
Si la réponse est non… on découpe.

Factoriser les interfaces

Même constat pour les interfaces TypeScript.

Dans une ancienne application, on peut facilement retrouver des modèles déclarés directement dans les composants :

users: {
id: number;
name: string;
email: string;
}[];

Alors qu’on pourrait avoir :

export interface User {
id: number;
name: string;
email: string;
}

Puis :

users: User[] = [];

C’est beaucoup plus lisible.
Et surtout, les interfaces deviennent réutilisables.

Factoriser les fonctions utils

Ah, les fameux fichiers utils.

On connaît tous :

utils.ts

Puis :

utils2.ts

Puis :

common-utils.ts

Puis :

helpers.ts

Et personne ne sait vraiment lequel utiliser. Et on ne parle même pas des fonctions que l’on trouve et dans des services/composants et dans des fichiers utils. Chaque développeur qui arrive amène sa pierre au projet… mais parfois sans regarder si la dite pierre n’est pas déjà dans un fichier utils.

La migration a été l’occasion de reprendre ces fonctions et de les classer : par domaine, par responsabilité.

Par exemple :

utils/
├── date.utils.ts
├── string.utils.ts
├── number.utils.ts
├── object.utils.ts
└── validation.utils.ts

Et surtout : supprimer les fonctions qui ne servent plus.

Une migration n’est pas uniquement l’occasion d’ajouter du nouveau code.
C’est aussi le moment de supprimer l’ancien. Pour une fois que l’on en a le temps… et le budget.

Améliorer la syntaxe des templates

Et là, Angular moderne apporte également de belles améliorations. On peut notamment remplacer les anciennes directives structurelles :

<div *ngIf="user">
{{ user.name }}
</div>

par la nouvelle syntaxe de contrôle :

@if (user) {
<div>
{{ user.name }}
</div>
}

Même chose pour les boucles.

Avant :

<div *ngFor="let user of users">
{{ user.name }}
</div>

Après :

@for (user of users; track user.id) {
<div>
{{ user.name }}
</div>
}

Angular fournit aujourd’hui des migrations dédiées pour cette nouvelle syntaxe de contrôle.
Et franchement… Après avoir repris énormément de vieux templates, cette syntaxe fait du bien, elle est plus lisible et ressemble davantage à ce qu’on connaît déjà côté TypeScript.

Améliorer la syntaxe TypeScript

La migration a également été l’occasion de moderniser le TypeScript. Et là encore, plusieurs années d’évolution se font sentir.

On retrouve progressivement :

  • des types plus précis ;
  • moins de any ;
  • des interfaces mieux définies ;
  • des fonctions typées ;
  • des propriétés mieux encapsulées ;
  • des optional chaining ;
  • des enums remplacés lorsque ce n’est pas nécessaire…

Par exemple :

Avant :

if (user && user.address && user.address.city) {
return user.address.city;
}

Après :

return user?.address?.city;

Ce n’est pas révolutionnaire. Mais multiplié par plusieurs milliers de lignes… ça change énormément la lisibilité.

Et les tests avec Angular 21 ?

Angular 21 a également fait évoluer l’écosystème du testing. Vitest est désormais le runner de tests principal dans Angular 21, et Angular travaille également sur la migration depuis Karma.

C’est donc un autre exemple du problème rencontré pendant cette migration : on ne migre pas simplement Angular, on migre tout l’écosystème.

Nettoyage du code et vérifications

Une fois toutes ces migrations terminées… Ce n’est toujours pas terminé.
Il reste encore une étape essentiel afin de repartir d’une base propre : nettoyer.

Personnellement j’utilise ESLint pour cela.

On vérifie :

  • les scripts inutilisés,
  • les imports inutiles,
  • les dépendances inutilisées,
  • les fichiers morts,
  • les TODO,
  • les any,
  • les commentaires obsolètes,
  • les configurations historiques,
  • les vieux polyfills,

Le passage à ESLint permet de repartir sur une base plus saine. Mais attention à ne pas vouloir corriger 10 000 warnings en une seule fois.

Le mieux est de procéder progressivement : corriger les erreurs bloquantes, ensuite les warnings, puis renforcer progressivement les règles. C’est long, c’est fastidieux… mais c’est nécessaire.

L’objectif n’est pas d’avoir un projet qui passe le lint uniquement parce qu’on a désactivé toutes les règles. (Oui on vous voit !)

On vérifie que le code compile, que le projet se build, sur notre poste et sur le serveur. Parce qu’une migration réussie n’est pas une migration qui compile sur mon ordinateur.

Le résultat

Au départ :

  • Angular 4.3
  • Ancienne infrastructure
  • Anciennes dépendances
  • NgModules partout
  • Templates historiques
  • Services énormes
  • Typage faible…

À l’arrivée :

  • Angular 21
  • Infrastructure moderne
  • Dépendances mises à jour
  • Composants standalone
  • Templates modernes
  • Tests modernisés
  • ESLint validé
  • Types renforcés
  • Code factorisé
  • Architecture plus claire

Et surtout : une application qui peut maintenant continuer à évoluer.

Parce que c’est finalement ça le vrai objectif d’une migration. Le vrai objectif, c’est de ne pas avoir à refaire exactement la même migration dans cinq ans parce qu’on a repoussé le problème.

Conclusion

Migrer une application Angular 4.3 vers Angular 21, c’est un challenge. Je ne pensais même pas que c’était possible au départ.

Il y a les changements Angular, mais il y a surtout tout ce qui gravite autour : Node.js, TypeScript, Docker, npm, les dépendances, les tests, le lint, les outils de build, l’architecture et le code métier.

Et finalement, le plus important dans ce genre de projet n’est pas de chercher à tout moderniser en une seule fois, c’est de se donner une feuille de route :

  • On avance étape par étape,
  • On compile,
  • On teste,
  • On nettoie,
  • On pleure,
  • On recommence.

Et petit à petit, on passe d’une application que plus personne ne voulait toucher … à une application Angular moderne. Et ça, franchement… ça fait du bien.

Checklist de migration

Pour résumer, voici la checklist que je garderais sous le coude pour une migration Angular importante :

☐ Vérifier Node.js
☐ Mettre à jour Docker
☐ Mettre à jour Angular CLI
☐ Mettre à jour Angular
☐ Vérifier TypeScript
☐ Mettre à jour les dépendances
☐ Adapter angular.json
☐ Vérifier le système de build
☐ Migrer les composants
☐ Migrer vers Standalone
☐ Factoriser les services
☐ Factoriser les interfaces
☐ Nettoyer les utils
☐ Moderniser les templates
☐ Moderniser TypeScript
☐ Améliorer le typage
☐ Revoir l'auto-complétion
☐ Migrer les tests unitaires
☐ Remplacer les tests E2E historiques
☐ Mettre en place ESLint
☐ Supprimer le code mort
☐ Lancer les tests
☐ Vérifier le build
☐ Vérifier la CI/CD
☐ Tester en recette
☐ Déployer 🚀

Et surtout : ne laissez pas votre application attendre Angular 21 pour commencer à migrer, faites les petites migrations régulièrement.
Votre futur vous remerciera.


En savoir plus sur Codequeen blog

📩 Reçois les articles CodeQueen directement dans ta boîte mail !

Laisser un commentaire

En savoir plus sur Codequeen blog

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Poursuivre la lecture