Axios est un outil puissant permettant de faire des requêtes HTTP, il simplifie la communication avec l’API et agit comme une surcouche du « fetch », que l’on peut facilement configurer. Voyons comment l’implémenter sous Typescript, puis l’utiliser avec React.

Nous allons partir d’un code simple qu’on pourrait trouver dans une application classique, puis faire un refactoring progressif et itératif, pour comprendre comment bâtir un système respectant les principes du « Clean Code », soit du code de qualité.

Notre cas de départ avec Axios

Dans une application React classique, on retrouve souvent une architecture simple, qui sépare les éléments selon leur rôle dans l’application. Je vous propose de partir d’un projet qui respecte l’architecture suivante:

  • Le dossier des pages, contenant directement les pages, ou bien avec un sous dossier par module.
    • Ici, il contient le module « Users », ainsi que trois pages: Index pour la liste des utilisateurs, Show pour un seul utilisateur, et Create pour le formulaire de création d’un seul utilisateur.
  • Le dossier des types, qui contient les différents types de votre application.
  • Enfin, le point d’entrée de votre application, App.tsx.

Concentrons-nous sur l’Index, soit l’affichage de tous les utilisateurs.

.
└── src/
    ├── pages/
    │   └── Users/
    │       ├── Create.tsx
    │       ├── Index.tsx
    │       └── Show.tsx
    ├── types/
    │   └── User.ts
    └── App.tsx

Voici comment est défini un de nos utilisateurs, dans User.ts:

export type User = {
	id: number;
	name: string;
};

Par ailleurs, voici à quoi ressemble la page Index, actuellement. Assez simple: un state pour les utilisateurs, une requête Axios qui nourrit le state, et nous affichons les utilisateurs dans une liste.

import { useEffect, useState } from 'react'
import axios from 'axios'
import { User } from '../types/User'

export default function Index() {
	const [users, setUsers] = useState<User[]>([])

	useEffect(() => {
		axios.get("http://localhost:4001/users").then(r => {
			setUsers(r.data)
		})
	}, [])

	return (
		<ul>
			{
				users.map(user => <li><a href={`/users/${user.id}`}>{user.name}</a></li>)
			}
		</ul>
	)
}

Conseils

Extraire le code dans un service

La première chose à faire, c’est de diviser le code dans des fichiers afin de le rendre réutilisable. Ce principe simple fait réellement la différence entre un code moyen et un bon code. Si on venait simplement à copier/coller notre requête à l’API, on court le risque de devoir aller modifier tous les fichiers où nous avons utilisé cette dite requête en cas de modification de l’API.

Commençons par ajouter un nouveau dossier, « services », dans lequel nous créons un fichier: « UserService.ts ». Ce service réunit tous les appels à notre API concernant les utilisateurs.

.
└── src/
    ├── pages/
    │   └── Users/
    │       ├── Create.tsx
    │       ├── Index.tsx
    │       └── Show.tsx
    ├── services/
    │   └── UserService.ts
    ├── types/
    │   └── User.ts
    └── App.tsx

Voici à quoi ressemble UserService.ts:

import axios from "axios";

export class UserService {
	public static index() {
		return axios.get(`http://localhost:4001/users`);
	}
}

Il s’agit d’une simple classe qui exporte une fonction statique: index. Cette méthode est ensuite utilisable à différents endroits de notre application. Remplaçons le code de la page d’affichage de tous les utilisateurs.

export default function Index() {
	// ...

	useEffect(() => {
		UserService.index().then(r => {
			setUsers(r.data)
		})
	}, [])

	// ...
}

Quelque chose de formidable vient de se produire: nous n’appelons pas directement Axios, car nous passons par un service. Si un jour, nous désirons utiliser une autre librairie pour faire nos requêtes API, il suffira simplement de modifier les Services, plutôt que passer dans tous les fichiers de notre application!

Moi-même adepte de la POO, je vais utiliser des classes dans cet article. Cependant, il est possible d’utiliser des fonctions, si vous préférez.

import axios from "axios";

export function index() {
	return axios.get(`http://localhost:4001/users`);
}

Typer les requêtes

Actuellement, nos requêtes renvoient une promesse de type AxiosResponse<any, any>>. Corrigeons ceci: any est notre ennemi. Nous ne savons pas quel est le type de retour de notre fonction index, et actuellement, nous pourrions être en train d’insérer n’importe quoi dans notre liste d’utilisateurs.

Axios renvoie une AxiosResponse<any, any> lorsque l'on ne précise rien.

Commençons par ajouter le type de retour à notre fonction index. Il s’agit simplement de rajouter un typage à notre requête. Typescript étant intelligent, le type de retour sera déduit de ce que notre requête renvoie.

export class UserService {
	public static index() {
		return axios.get<User[]>(`http://localhost:4001/users`);
	}
}

C’est bien mieux! Notre data est désormais de type User[]. Si nos useState sont également typés, nous sommes protégés contre notre propre bêtise.

Axios renvoie le bon type: un tableau d'utilisateurs.

Typer les paramètres

Lorsque nous appelons les fonctions proposées par notre service, nous ne voulons pas avoir à vérifier l’API en permanence pour vérifier que nous envoyons les bonnes données: notre service doit nous dire ce qu’il nous faut d’emblée.

Profitons-en pour ajouter une autre fonction utile à notre service: le store, qui permet la création d’un utilisateur. C’est peut-être le moment de se remémorer la documentation d’axios, qui met à notre service les fonctions get et post. C’est ce que nous allons utiliser pour la création: une requête POST.

export class UserService {
	public static index() {
		return axios.get<User[]>(`http://localhost:4001/users`);
	}

	public static store(user: User) {
		return axios.post<User>(`http://localhost:4001/users`, user);
	}
}

Seulement, un souci se pose à nous: nous tentons d’envoyer un User complet. Pourtant, lors de la création d’un utilisateur, nous n’avons pas encore de champ « id » à notre disposition! Comment faire? Certains proposeront de régler le souci en rendant les champs de notre User optionnels. Surtout pas! Il s’agirait d’un mensonge: l’id n’est pas optionnel, il est absent.

Nous allons alors utiliser le type utilitaire Omit, qui permet de faire exactement ceci: dire que le champ id est absent. Les autres champs sont bel et bien nécessaires, cependant.

export class UserService {
	public static index() {
		return axios.get<User[]>(`http://localhost:4001/users`);
	}

	public static store(user: Omit<User, "id">) {
		return axios.post<User>(`http://localhost:4001/users`, user);
	}
}

Cependant, le type de retour reste bien un User complet: l’API nous renvoie l’utilisateur créé en base de données, avec un id et un name.

Pour aller plus loin, on peut même extraire ce type jusqu’au fichier User.ts, afin de le réutiliser dans les pages où ceci convient: le formulaire de création d’un nouvel utilisateur, par exemple.

export type User = {
	id: number; 
	name: string;
};

export type NewUser = Omit<User, "id">;
export class UserService {
	public static index() {
		return axios.get<User[]>(`http://localhost:4001/users`);
	}

	public static store(user: NewUser) {
		return axios.post<User>(`http://localhost:4001/users`, user);
	}
}

Extraire axios

Un détail devrait vous chagriner: l’URL est répété plusieurs fois. Le jour où l’URL change, il faudra repasser sur tous les fichiers de services. Pour éviter ceci, créons un fichier qui va exporter notre instance d’axios pré-configurée. Ce fichier s’appelle axios.ts, et se situe dans src/api/.

import axios from "axios";

export const instance = axios.create({
	baseURL: 'http://localhost:4001/',
})

Et mettons à jour nos routes pour refléter cette modification:

export class UserService {
	public static index() {
		return instance.get<User[]>(`/users`);
	}

	public static store(user: NewUser) {
		return instance.post<User>(`/users`, user);
	}
}

Gérer les erreurs

Maintenant que nous avons extrait l’instance d’axios, nous pouvons gérer les erreurs de manière globale. Pour ce faire, nous allons utiliser les intercepteurs axios. Les intercepteurs agissent comme un middleware, ils s’interposent entre le résultat de la requête et notre réception. On peut alors agir sur ce qui est renvoyé par l’API, juste avant de le recevoir au niveau de l’application.

Voici un exemple de ce que l’on pourrait produire: en cas de succès, nous envoyons un console.info pour nous signaler que la requête est bonne. Un cas d’utilisation intéressant est de faire des conversions de données à cet endroit: on peut transformer un string en Date, par exemple.

En cas d’erreur, nous pouvons également effectuer un traitement: écrire dans les logs de l’application, rediriger sur la page d’accueil, afficher une Snackbar qui nous prévient de l’erreur… Ici, nous affichons simplement une erreur dans la console.

import axios from "axios";

const instance = axios.create({
	baseURL: 'http://localhost:4001/',
})

axios.interceptors.response.use(
	(response) => {
		console.info("Réponse OK");
		return response;
	},
	(error) => {
		console.error("Erreur détectée!");
		return Promise.reject(error);
	}
);

export { instance };

Résultat final

On peut désormais appliquer cette logique pour créer le reste de nos routes. Voici une nouvelle version de notre service, qui utilise et respecte les méthodes HTTP à notre disposition.

import { instance } from "../api/axios";
import { User } from "../types/User";

export class UserService {
	public static index() {
		return instance.get<User[]>(`/users`);
	}

	public static store(user: NewUser) {
		return instance.post<User>(`/users`, user);
	}

	public static show(id: number) {
		return instance.get<User>(`/users/${id}`);
	}

	public static update(user: User) {
		return instance.put<User>(`/users/${user.id}`, user);
	}

	public static delete(id: User["id"]) {
		return instance.delete(`/users/${id}`);
	}
}

Conclusion

Ces quelques conseils de programmation s’appliquent globalement à tous les cas que vous rencontrerez: extraire votre code afin de le séparer est souvent de bon ton, attention cependant à faire la part des choses: vouloir trop séparer va rendre le code complexe, difficile à utiliser et naviguer, et les modifications seront lourdes.

Je vous conseille ainsi de réfléchir avant de séparer votre code: en avez-vous réellement besoin?

Si vous désirez aller plus loin, je vous propose quelques ressources.