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 : العربية

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.
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 devDans 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 :
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.
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.
StrictModedé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 pasStrictModepour le cacher. - Pas de
asyncdirectement devant la fonction deuseEffect: elle doit renvoyer une fonction de nettoyage ou rien, alors qu'une fonctionasyncrenvoie toujours une Promise. On écrit une fonctionasyncà 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 :
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.
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
UNIQUEde 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.
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
POSTvers/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.
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
localStorageexpose 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.
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. fetchne rejette pas sur une erreur HTTP : vérifieresponse.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
- Node.js et Express pour un développeur PHP : l'API, MySQL, bcrypt, sessions (E14).
- E22 : JavaScript,
map,filteretreduce, indispensables en React (page de l'épisode). - Premier projet React : du JavaScript au panier : JSX, props,
useState(E27). - Cet article : React consomme l'API, CRUD et login JWT (E28).
Le parcours : Développement Web Full Stack.
Pour aller plus loin
- Apprendre avec l'IA sans perdre la main : coder avec ChatGPT sans copier-coller.
Sources
Quiz : 3 questions pour vérifier
Rien n’est envoyé : tes réponses restent dans ton navigateur.
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.



