27/03/2023 Tech
Pourquoi transformer les données externes (API, base de données, json) vers nos propres objets JS ?
Le but de cet article est de montrer l’intérêt de travailler avec nos propres objets formatés dans notre application JS et pas directement avec la structure des données externes.
Introduction
A travers les divers projets auxquelles j’ai participé, j’ai pu voir quelques fois que les données API étaient utilisées directement dans le code ou le template HTML, sans traitement préalable dessus. Le projet fonctionne mais cela créer une dépendance forte à l’API sur laquelle est branchée l’application. Cette façon de faire peut rendre la maintenance et l’évolution du projet plus compliqué. Si la structure des données de l’API change par exemple, les changements à faire peuvent être dispatchés un peu partout dans le code de l’application.
Ce que j’essaie de pousser sur les projets et que je vous expose ici est d’insérer une couche de traitement relativement simple entre les données reçus de l’API et l’application. Nous verrons aussi les autres intérêts que l’on peut en tirer comme “mocker” plus facilement les API et commencer à travailler sans connaitre la structure exacte de leurs données.
Mise en place à travers un exemple
Dans cet exemple, nous allons travailler avec Typescript pour définir nos objets et React pour le rendu visuel. (Je propose une solution équivalente sans typescript un peu plus bas dans l’article).
L’exemple est simple, on appel une api qui nous renvoi une liste d’objets d’artistes et on affiche cette liste.
Voici le code :
- un fichier api.ts qui s’occupe d’appeler l’API et retourner une promesse avec les données brutes non traités sous forme de json
// api.ts
export const getArtists = (): Promise<ArtistApi[]> => {
return fetch(`/artists`).then((response: Response) => {
return response.json();
}).then((json: { list: ArtistApi[] }) => {
return json.list;
});
}
- un fichier types.ts qui contient l’interface ArtistApi renvoyé par l'API
// types.ts
export interface ArtistApi {
name: string;
last_update_date: string;
informations: {
perso: {
description?: string;
email?: string;
},
},
medias: {
photo1?: string;
photo2?: string;
photo3?: string;
},
}
- un fichier ListArtists.tsx en React pour afficher les données récupérées de l’API :
// ListArtists.tsx
export const ListArtists = (): JSX.Element => {
const [artists, setArtists] = useState<ArtistApi[]>([]);
useEffect(() => {
api.getArtists().then((list: ArtistApi[]) => {
setArtists(list);
});
}, []);
return (
<ul>
{artists.map((artist) => <li>
<h2>{artist.name}</h2>
{artist.informations.perso.description && <p>{artist.informations.perso.description}</p>}
{artist.informations.perso.email && <p>{artist.informations.perso.email}</p>}
{artist.last_update_date && <p>{artist.last_update_date}</p>}
{artist.medias.photo1 && <img src={artist.medias.photo1} />}
{artist.medias.photo2 && <img src={artist.medias.photo2} />}
{artist.medias.photo3 && <img src={artist.medias.photo3} />}
</li>)}
</ul>
);
}
Pour casser cette forte dépendance, le principe est de ne pas utiliser le type ArtisteApi en dehors du fichier api.ts. On va créer un autre type correspondant Artist qui lui sera utilisé et partagé dans toute l'application.
Avoir notre propre type permet aussi d’avoir un objet propre à manipuler car les données de L’API ne respectent pas forcément les règles de nommage du projet et peuvent aussi contenir des données superflus non consommées.
Voici le nouveau type simplifié :
// types.ts
export interface Artist {
name: string;
lastUpdateDate: string;
description: string;
email: string;
photos: string[];
}
Pour bien séparer le code concernant l’API, il est préférable de créer un répertoire api et d’y mettre tous les fichiers la concernant dedans. On y met api.ts et un fichier types.ts qui contient le type ArtistApi.
On va créer une nouvelle couche de traitement à l’aide d’un nouveau fichier apiController.ts, qui exporte une méthode getArtists (même nom que celle de l’API) et qui appel getArtists de api.ts. On transforme les données reçues. Pour cela on utilise la méthode formatArtist (qu’on va créer dans un autre fichier) pour transformer le type ArtistApi en type Artist.
C’est ce controller qui sera maintenant appeler pour récupérer des données de l’API :
// controller/apiController.ts
export const getArtists = (): Promise<Artist[]> => {
return api.getArtists().then((list: ArtistApi[]) => {
return list.map((artist: ArtistApi) => {
return formatArtist(artist);
});
});
}
On créer un nouveau fichier api/formatters.ts avec la méthode formatArtist qui prends en entrer un objet de type ArtistApi et qui retourne un objet de type Artist Cette fonction va nous permettre de transformer l’objet reçu de l’API vers notre objet
// api/formatters.ts
export const formatArtist = (artist: ArtistApi): Artist =>
const photos: string[] = [];
artist.medias.photo1 && photos.push(artist.medias.photo1);
artist.medias.photo2 && photos.push(artist.medias.photo2);
artist.medias.photo3 && photos.push(artist.medias.photo3);
return {
name: artist.name,
lastUpdateDate: artist.last_update_date,
description: artist.informations.perso.description,
email: artist.informations.perso.email,
photos,
};
}
Ensuite dans ListArtists.tsx on utilise le controller controller/apiController.ts et le nouveau type Artist :
// ListArtists.tsx
export const ListArtists = (): JSX.Element => {
const [artists, setArtists] = useState<Artist[]>([]);
useEffect(() => {
apiController.getArtists().then((list: Artist[]) => {
setArtists(list);
});
}, []);
return (
<ul>
{artists.map((artist) => <li>
<h2>{artist.name}</h2>
{artist.lastUpdateDate && <p>{artist.lastUpdateDate}</p>}
{artist.description && <p>{artist.description}</p>}
{artist.email && <p>{artist.email}</p>}
{artist.photos.map((photo) => <img src={photo} />)}
</li>)}
</ul>
);
}
De cette manière le travail sur la structure de l’api est limité au fichier controller/apiController.ts, le traitement est centralisé uniquement à un endroit. Par exemple si un jour la structure de l’objet ArtistApi change, il faudra seulement modifier la fonction de formatage formatArtist.
Exemple sans Typescript
Même si Typescript est fortement conseillé, il peut arriver que l’on travail sur un projet assez ancien et sur lequel il n’est pas possible de mettre typescript.
Dans ce cas, le code ne change pas beaucoup. Il suffit de retirer les types.
Je propose tout de même de rajouter quelque chose pour connaitre la structure des données manipulées par notre application.
Pour cela je propose d’utiliser les ‘pseudos’ Classes JS. Pour notre type Artist on créer une Classe artist.js qu'on met dans un répertoire model :
// model/artist.js
export class Artist {
constructor() {
this.name = undefined;
this.lastUpdateDate = undefined;
this.description = undefined;
this.email = undefined;
this.photos = [];
}
}
Notre fonction de formatageformatArtist de api/formatters.js utilise une instance de Artist pour renvoyer le nouvel objet :
// api/formatters.js
// @return Instance of Artist (models/artist.js)
export const formatArtist = (artistApi) => {
const artist = new Artist();
artist.name = artistApi.name;
artist.lastUpdateDate = artistApi.last_update_date;
artist.description = artist.informations.perso.description,
artist.email = artist.informations.perso.email,
artist.medias.photo1 && artist.photos.push(artist.medias.photo1);
artist.medias.photo2 && artist.photos.push(artist.medias.photo2);
artist.medias.photo3 && artist.photos.push(artist.medias.photo3);
return artist;
}
Ainsi l’application manipule des instances de Artist. On connait la structure des données manipulées partout dans notre application grâce aux Classes dans le répertoire models.
Les autres intérêts
Outre l’intérêt de centraliser à un seul endroit le traitement des données API, un des intérêts est de rendre possible le développement du front sans connaitre exactement quelle sera la structure des données API. Il faudra bien-sur créer vos objets en partant de ce qui doit être affiché et ce dont vous avez besoin.
Si on reprends notre exemple, imaginons que l’API n’est pas disponible et que l’on ne connait pas la structure qu’elle nous renvoi, controller/apiController.ts ressemblera à ça :
// controller/apiController.ts
export const getArtists = (): Promise<Artist[]> => {
return new Promise((resolve) => {
resolve([
{
name: 'test',
lastUpdateDate: '20/02/2022',
description: 'test description1',
email: 'email@test.com',
photos: [],
},
{
name: 'test2',
lastUpdateDate: '20/02/2022',
description: 'test2 description2',
email: 'email@test.com',
photos: ['http://testimage'],
}
]);
});
}
Cela permet de commencer le développement de l’interface en se basant sur un type définitif Artist. Vous devriez pouvoir déterminer les types à partir des données à afficher et des spécifications. Les appels AJAX et le formatage pourront être ajoutés plus tard. Cela touchera uniquement les fichiers api/api.ts et controller/apiController.ts (avec des ajouts d'autres fichiers pour le formatage et les types de l'API).
Les inconvénients
Les inconvénients sont plutôt limités. Le premier est que cela ajoute un peu plus de code pour le formatage, mais en face le gain est plutôt grand surtout pour des gros projets.
Le deuxième inconvénient est relatif à certains cas.
Si votre application doit réagir en temps réel avec une fréquence élevée.
Par exemple votre interface doit se mettre à jour toute les 100ms à partir de données (écoute continue par WebSocket). Dans ce cas il serait peut être préférable de ne pas transformer les données reçues et les exploiter telles quelles pour des raisons de performances, car le formatage peut être plus ou moins couteux.
Pour aller plus loin
Si vous avez la main sur le développement de la partie serveur qui sert l’API, il existe des solutions intéressantes comme GraphQL (https://graphql.org/) qui permet de demander la structure des données que l’on veut et obtenir seulement ce dont on a besoin.
Il y a aussi tRPC (https://trpc.io/) avec ZOD (https://zod.dev/) qui aide à “typer“ et formater les données échangées entre le serveur et le client.
Ces solutions permettent aussi de gagner en performance dans les échanges avec l’API.
Conclusion
Ce qu’il faut retenir :
– On diminue notre dépendance à l’API
– Permet de manipuler des objets “propres” dans l’application
– Permet une meilleur maintenabilité dans le temps
– Possibilité de développer notre application sans attendre que l’API soit prête
J’espère que cet article vous a permis de comprendre l’intérêt de cette pratique et vous aidera à la mettre en place dans vos projets. La structure en exemple n’est qu’une proposition, libre à vous d’organiser le code suivant votre projet, tant que le principe est la.
Pourquoi transformer les données externes (API, base de données, json) vers nos propres objets JS ? was originally published in ekino-france on Medium, where people are continuing the conversation by highlighting and responding to this story.