Parcours12 min de lecture

Premier projet Node.js + React : API, CRUD et login JWT

Ton premier projet full stack pas à pas : React consomme une API Node.js/Express, avec useEffect, CRUD, erreurs, login JWT et header Bearer.

Aussi en : العربية

Mehdi en tenue de formateur, couverture de l’article : React + Node.js

Ton premier projet full stack peut tenir en deux dossiers : une API Node.js/Express qui répond en JSON, et un front React qui la consomme. Dans le cours E28, React lit, ajoute, modifie et supprime les utilisateurs d'une vraie base MySQL, puis se connecte avec un token JWT. L'API est celle du cours PHP → Node (E14), reconstruite pour répondre en JSON, et le front s'appuie sur les bases React d'E27.

Comment lancer le projet ?

Node.js 24 et MySQL 8 sont nécessaires ; sans MySQL installé, docker compose up -d démarre la base depuis le dossier du dépôt.

sh
git clone https://github.com/Laraibi/lara-academy-react-api.git lara-react-api
cd lara-react-api/api
cp .env.example .env   # puis remplacer JWT_SECRET
npm ci
npm run db:init
npm run dev

Dans un deuxième terminal : cd web, npm ci, npm run dev. Chaque étape de la vidéo est un tag : git checkout ch03-effect te place sur l'étape useEffect, git diff ch03-effect ch04-etats montre ce qui change.

Pourquoi une seule fetch envoie-t-elle des milliers de requêtes ?

Le cours s'ouvre sur un piège classique. Hamza écrit une seule fetch pour récupérer les utilisateurs, la liste s'affiche… et l'onglet Network compte des milliers de requêtes en quelques secondes.

La boucle, reconstituée par Bob et Hamza : React appelle le component pour calculer l'affichage, y trouve la fetch ; la réponse arrive, setUsers demande un nouveau rendu ; le nouveau rendu retrouve la fetch. Le rendu doit seulement calculer l'interface ; une requête au serveur est un autre travail, qui a sa propre place.

Que change une API qui répond en JSON ?

Dans le cours Node, le serveur envoyait une page toute prête avec res.render. Ici, il n'envoie que les données avec res.json, et React décide de l'affichage. Les routes suivent les verbes HTTP :

js
router.use(requireJwt);   // ajouté au chapitre Bearer

router.get('/', users.index);
router.post('/', users.store);
router.put('/:id', users.update);
router.delete('/:id', users.destroy);

Avec un formulaire HTML, la modification et la suppression passaient par POST ; avec JSON, on utilise PUT et DELETE. Et le JSON des utilisateurs contient id, nom et email, jamais le hash du mot de passe : le model choisit ses colonnes.

Comment stopper la boucle avec useEffect ?

Le code placé dans useEffect s'exécute après l'affichage. Le tableau en second argument est la liste des dépendances : vide, l'effet ne s'exécute qu'à l'apparition du component.

jsx
useEffect(() => {
  let ignore = false;

  async function loadUsers() {
    setLoading(true);
    setError('');
    try {
      const data = await request('/api/users', { token });
      if (!ignore) setUsers(data);
    } catch (loadError) {
      if (!ignore) setError(loadError.message);
    } finally {
      if (!ignore) setLoading(false);
    }
  }

  loadUsers();
  return () => { ignore = true; };
}, [token, attempt]);

Trois points expliqués dans le cours :

  • Deux requêtes en développement : ce n'est pas un bug. StrictMode démarre l'effet, le nettoie, puis le redémarre pour vérifier qu'on ne laisse rien derrière soi. En production, il en reste une. Ne retire pas StrictMode pour le cacher.
  • Pas de async directement devant la fonction de useEffect : elle doit renvoyer une fonction de nettoyage ou rien, alors qu'une fonction async renvoie toujours une Promise. On écrit une fonction async à l'intérieur et on l'appelle.
  • ignore : le nettoyage du premier passage le met à true, donc sa réponse ne modifie pas le state.

Pourquoi fetch et pas Axios ? fetch est déjà dans le navigateur, sans rien installer.

Chargement et erreurs : pourquoi response.ok ?

Bob utilise DevTools comme laboratoire : F12, onglet Network, et à la place de « No throttling », un profil 3G. Page rechargée : écran vide. Est-ce cassé ou en cours de chargement ? L'utilisateur doit le savoir. Chaque requête a trois états : on attend, les données sont arrivées, ou il y a eu un problème. D'où le state loading, remis à false dans finally. (Pense à revenir à « No throttling » ensuite.)

Puis Bob coupe MySQL. Tout le monde s'attend à passer dans le catch… mais Network affiche 500 et fetch se résout normalement. Le serveur a répondu, donc pour fetch la requête est arrivée. C'est à nous de tester response.ok. Le cours regroupe ce contrôle dans une fonction request, dans api.js :

js
export async function request(path, { token, method = 'GET', body } = {}) {
  const headers = {};
  if (body) headers['Content-Type'] = 'application/json';
  if (token) headers.Authorization = `Bearer ${token}`;

  const response = await fetch(path, {
    method,
    headers,
    body: body ? JSON.stringify(body) : undefined,
  });

  if (!response.ok) {
    const data = await response.json().catch(() => ({}));
    const error = new Error(data.error ?? `Erreur ${response.status}`);
    error.status = response.status;
    throw error;
  }
  return response.status === 204 ? null : response.json();
}

Quand fetch rejette-t-elle seule ? Quand la réponse n'arrive pas du tout : en mode Offline, Chrome affiche Failed to fetch. Un bouton Réessayer incrémente attempt, qui est dans les dépendances de l'effet, donc la requête repart sans recharger la page.

Ajouter, modifier, supprimer : où placer la requête ?

Question posée par Mehdi : qu'est-ce qui a déclenché cette requête ? L'affichage de la page, ou le clic de l'utilisateur ? Pour un ajout, c'est le clic : la requête va donc dans le handler du formulaire, pas dans un useEffect, comme le recommande la documentation de React.

jsx
async function handleAdd(values) {
  const user = await request('/api/users', { token, method: 'POST', body: values });
  setUsers(previous => [...previous, user]);
}

async function handleSave(id, values) {
  const saved = await request(`/api/users/${id}`, { token, method: 'PUT', body: values });
  setUsers(previous => previous.map(user => (user.id === id ? saved : user)));
}

async function handleDelete(id) {
  await request(`/api/users/${id}`, { token, method: 'DELETE' });
  setUsers(previous => previous.filter(user => user.id !== id));
}
  • Le serveur renvoie l'utilisateur créé avec son id : on l'ajoute au state avec un nouveau tableau, comme le panier d'E27, sans recharger toute la liste.
  • L'email de Sara une deuxième fois : la contrainte UNIQUE de la table produit une 409 et le message « Cet email existe déjà. ». Le message vient du serveur ; React l'affiche, et le formulaire reste rempli pour corriger.
  • Après un PUT, Bob recharge la page : le nouveau nom est toujours là, preuve que la base l'a enregistré.
  • Pour le DELETE, on retire la ligne après la réponse du serveur. La retirer avant serait plus rapide à l'écran, mais il faudrait la remettre en cas d'échec : au début, reste simple.

Puis Bob remarque : « Je n'ai tapé aucun mot de passe, et j'ai supprimé quelqu'un. »

Comment fonctionne le login avec JWT ?

Rappel du cours Node : la session est un badge dont le serveur se souvient ; le JWT est une carte signée que le client garde avec lui.

js
export async function login(req, res) {
  const { email, password } = req.body;
  const user = email && password ? await User.findByEmailWithPassword(email) : undefined;
  const valid = user ? await bcrypt.compare(password, user.password) : false;
  if (!valid) return res.status(401).json({ error: 'Email ou mot de passe incorrect.' });

  const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
  res.json({ token, user: { id: user.id, nom: user.nom, email: user.email } });
}
  • Email et mot de passe partent en POST vers /api/login : ce sont des informations secrètes qui ne doivent pas apparaître dans l'URL.
  • Le token est signé, pas caché : n'importe qui peut lire son contenu. On n'y met jamais de mot de passe ; ici, seulement userId, avec une validité d'une heure. Le secret de signature reste dans .env.
  • Mauvais mot de passe ou email inconnu : le même message. Le formulaire de login ne révèle pas si un email existe.
  • Côté React, la réponse est rangée dans un state nommé session (un simple nom, pas la session du serveur) : rempli, on affiche la liste ; vide, le formulaire.

Bob teste quand même : un DELETE envoyé avec curl depuis le terminal, sans token et sans React… passe. Le login seul ne protège rien.

Pourquoi 401 alors que je suis connecté ?

La vraie protection est sur le serveur. React tourne dans le navigateur, et n'importe qui peut envoyer une requête sans notre interface. On ajoute une ligne, router.use(requireJwt), et le même curl reçoit 401.

js
export function requireJwt(req, res, next) {
  const [scheme, token] = (req.headers.authorization ?? '').split(' ');
  if (scheme !== 'Bearer' || !token) {
    return res.status(401).json({ error: 'Connexion requise.' });
  }
  try {
    req.userId = jwt.verify(token, process.env.JWT_SECRET).userId;
    next();
  } catch {
    res.status(401).json({ error: 'Connexion requise.' });
  }
}

Surprise : après reconnexion, React reçoit aussi 401. Le token est bien dans le state… mais dans l'onglet Headers de la requête, aucun Authorization. Le token ne part pas tout seul : c'est toi qui dois le mettre dans la requête. D'où la ligne Authorization: Bearer <token> dans request, le token passé de App à Users par props, et ajouté aux dépendances de l'effet.

Ce que le cours fait aussi observer :

  • un seul caractère modifié dans le token → 401 ;
  • Déconnexion vide le state, mais un token copié reste valide jusqu'à son expiration (une heure ici) : ce n'est pas un badge de session que le serveur peut révoquer ;
  • au rechargement, le state en mémoire disparaît, donc retour au formulaire. Le cours garde ce choix pour la clarté et signale que localStorage expose le token à tout JavaScript de la page en cas de XSS : à étudier avant de choisir.

Faut-il un useEffect pour filtrer la liste ?

Dernier chapitre : Bob veut une recherche et propose un state filteredUsers mis à jour par un useEffect. Question de Mehdi : y a-t-il ici une requête ou quelque chose d'extérieur à React ? Non : le résultat se calcule à partir de users et query.

jsx
const search = query.trim().toLowerCase();
const visibleUsers = users.filter(user => user.nom.toLowerCase().includes(search));

La règle à retenir : ce qui se calcule reste dans le rendu ; ce qui vient d'un clic va dans un handler ; useEffect sert à se synchroniser avec l'extérieur de React, comme la requête au chargement de la page.

Questions fréquentes

Pourquoi deux requêtes au lieu d'une avec useEffect ?

En développement, StrictMode lance l'effet, le nettoie et le relance. En production (npm run build puis npm run preview), il n'y en a qu'une.

Où stocker le token JWT dans React ?

Dans ce cours, dans le state, en mémoire : il disparaît au rechargement. localStorage est évoqué comme question, avec son risque en cas de XSS, mais n'est pas implémenté.

Session ou JWT ?

La session reste connue du serveur, qui peut l'invalider. Le JWT est une carte signée portée par le client et valable jusqu'à son expiration. Le cours Node présente les deux ; celui-ci implémente le JWT avec React.

Le projet utilise-t-il MongoDB ou Axios ?

Non : l'API utilise MySQL, et le front utilise fetch, sans autre dépendance que React.

À retenir

  • Une requête au chargement va dans useEffect ; une requête déclenchée par un clic va dans le handler.
  • fetch ne rejette pas sur une erreur HTTP : vérifie response.ok.
  • Trois états par requête : chargement, données, erreur.
  • La protection se fait sur le serveur (requireJwt), jamais seulement en cachant un bouton.
  • Le token doit être ajouté à chaque requête : Authorization: Bearer <token>.

Regarder le cours

Le cours complet (14 min 41, en darija, avec Mehdi, Hamza et Bob) est sur sa page : Du fetch en boucle au login JWT : React + API (E28).

  • 00:00 Une seule fetch, des requêtes sans fin
  • 00:31 Le serveur répond en JSON : de res.render à res.json
  • 01:25 useEffect : stopper la boucle (dépendances, StrictMode, async)
  • 03:21 Réseau lent avec DevTools, chargement, erreurs et response.ok
  • 06:12 Ajouter : POST et email en double (409)
  • 07:32 Modifier et supprimer : PUT, DELETE et contrôle en base
  • 08:54 Login : POST /api/login et token JWT
  • 10:52 Bearer et 401 : envoyer le token dans la requête
  • 12:54 Recherche : ce qui ne va pas dans un effet
  • 13:42 Récap et défi

Le projet complet, étape par étape

  1. Node.js et Express pour un développeur PHP : l'API, MySQL, bcrypt, sessions (E14).
  2. E22 : JavaScript, map, filter et reduce, indispensables en React (page de l'épisode).
  3. Premier projet React : du JavaScript au panier : JSX, props, useState (E27).
  4. Cet article : React consomme l'API, CRUD et login JWT (E28).

Le parcours : Développement Web Full Stack.

Pour aller plus loin

Sources

Quiz : 3 questions pour vérifier

Rien n’est envoyé : tes réponses restent dans ton navigateur.

  1. Pourquoi une fetch écrite directement dans le corps d'un component envoie-t-elle des requêtes sans s'arrêter ?
  2. Le serveur répond 500. Que fait fetch ?
  3. On cache un bouton Supprimer dans React. L'API est-elle protégée ?

Partager

Rédigé à partir du cours vidéo et de sa transcription, avec l’aide d’outils d’IA, relu et vérifié par Mehdi Laraibi le 4 novembre 2026.