Get your free and exclusive 80-page Banking Passkey Report
react express crud app mysql

Application CRUD avec React, Node.js, Express et MySQL

Ce tutoriel vous apprend à créer une application CRUD de base en utilisant React pour le frontend et Express pour le backend avec une base de données MySQL.

Blog-Post-Author

Lukas

Created: June 17, 2025

Updated: June 24, 2025


Dans ce tutoriel complet, vous apprendrez à créer une application CRUD (Create, Read, Update, Delete) full-stack en utilisant React pour le front-end et Node.js avec Express pour le back-end, le tout alimenté par une base de données MySQL. Nous utiliserons des technologies telles que React Router, Axios et TailwindCSS pour améliorer l'expérience utilisateur.

Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

1. Aperçu du projet#

Nous allons développer une application full-stack où les utilisateurs pourront gérer des tutoriels. Chaque tutoriel aura les attributs suivants :

  • ID : Identifiant unique
  • Titre : Nom du tutoriel
  • Description : Informations détaillées
  • Statut de publication : Indique si le tutoriel est publié

Les utilisateurs peuvent effectuer les actions suivantes :

  • Créer un nouveau tutoriel
  • Récupérer tous les tutoriels ou un tutoriel spécifique par son ID
  • Mettre à jour les tutoriels existants
  • Supprimer des tutoriels
  • Rechercher des tutoriels par titre
Subreddit Icon

Discuss passkeys news and questions in r/passkey.

Join Subreddit

Ci-dessous, vous trouverez quelques captures d'écran d'exemple :

1.1 Ajouter un nouveau tutoriel#

1.2 Afficher tous les tutoriels#

Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

1.3 Modifier un tutoriel#

1.4 Rechercher des tutoriels par titre#

Substack Icon

Subscribe to our Passkeys Substack for the latest news.

Subscribe

2. Architecture#

L'application suit une architecture client-serveur :

  • Backend : Node.js avec Express gère les API RESTful et interagit avec la base de données MySQL en utilisant l'ORM Sequelize.
  • Frontend : React.js communique avec le backend via Axios pour les requêtes HTTP et utilise React Router pour la navigation.

3. Implémentation du backend#

3.1 Configuration de l'application Node.js#

  1. Créer le répertoire du projet :

    mkdir react-node-express-mysql-crud cd react-node-express-mysql-crud
  2. Initialiser l'application Node.js :

    npm init -y
  3. Installer les dépendances :

    npm install express sequelize mysql2 cors --save
  4. Utiliser la syntaxe ESModule Ajoutez la ligne suivante à votre fichier package.json :

    { "type": "module" // ... }

3.2 Configurer MySQL & Sequelize#

  1. Créer le fichier de configuration (app/config/db.config.js) :
export default { HOST: "localhost", USER: "root", PASSWORD: "root", DB: "db", PORT: 8081, dialect: "mysql", pool: { max: 5, min: 0, acquire: 30000, idle: 10000, }, };
  1. Initialiser Sequelize (app/models/index.js) :
import dbConfig from "../config/db.config.js"; import Sequelize from "sequelize"; import Tutorial from "./tutorial.model.js"; const sequelize = new Sequelize(dbConfig.DB, dbConfig.USER, dbConfig.PASSWORD, { host: dbConfig.HOST, dialect: dbConfig.dialect, pool: dbConfig.pool, port: dbConfig.PORT, }); const db = {}; db.Sequelize = Sequelize; db.sequelize = sequelize; db.tutorials = Tutorial(sequelize, Sequelize); export default db;
  1. Définir le modèle Tutorial (app/models/tutorial.model.js) :
export default (sequelize, Sequelize) => { const Tutorial = sequelize.define("tutorial", { title: { type: Sequelize.STRING, }, description: { type: Sequelize.STRING, }, published: { type: Sequelize.BOOLEAN, }, }); return Tutorial; };

Si vous n'avez pas de base de données MySQL pour le développement local, vous pouvez utiliser Docker pour créer un conteneur MySQL temporaire :

docker run --rm -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root \ -e MYSQL_DATABASE=db \ mysql:8

3.3 Définir les routes et les contrôleurs#

  1. Créer le contrôleur (app/controllers/tutorial.controller.js) :
import db from "../models/index.js"; const Op = db.Sequelize.Op; const Tutorial = db.tutorials; // Créer et enregistrer un nouveau tutoriel export const create = (req, res) => { // Valider la requête if (!req.body.title) { res.status(400).send({ message: "Content can not be empty!", }); return; } // Créer un tutoriel const tutorial = { title: req.body.title, description: req.body.description, published: req.body.published ? req.body.published : false, }; // Enregistrer le tutoriel dans la base de données Tutorial.create(tutorial) .then((data) => { res.send(data); }) .catch((err) => { res.status(500).send({ message: err.message || "Some error occurred while creating the Tutorial.", }); }); }; // Récupérer tous les tutoriels export const findAll = (req, res) => { // Autoriser une condition de filtre via un paramètre de requête const title = req.query.title; const condition = title ? { title: { [Op.like]: `%${title}%` } } : null; Tutorial.findAll({ where: condition }) .then((data) => { res.send(data); }) .catch((err) => { res.status(500).send({ message: err.message || "Some error occurred while retrieving tutorials.", }); }); }; // Trouver un seul tutoriel par son ID export const findOne = (req, res) => { const id = req.params.id; // Trouver le tutoriel par sa clé primaire Tutorial.findByPk(id) .then((data) => { if (data) { res.send(data); } else { res.status(404).send({ message: `Cannot find Tutorial with id=${id}.`, }); } }) .catch((err) => { res.status(500).send({ message: "Error retrieving Tutorial with id=" + id, }); }); }; // Mettre à jour un tutoriel par son ID export const update = (req, res) => { const id = req.params.id; // Mettre à jour le tutoriel avec l'ID spécifié Tutorial.update(req.body, { where: { id: id }, }) .then((num) => { if (num == 1) { res.send({ message: "Tutorial was updated successfully.", }); } else { res.send({ message: `Cannot update Tutorial with id=${id}. Maybe Tutorial was not found or req.body is empty!`, }); } }) .catch((err) => { res.status(500).send({ message: "Error updating Tutorial with id=" + id, }); }); }; // Supprimer un tutoriel par son ID export const deleteOne = (req, res) => { const id = req.params.id; // Supprimer le tutoriel avec l'ID spécifié Tutorial.destroy({ where: { id: id }, }) .then((num) => { if (num == 1) { res.send({ message: "Tutorial was deleted successfully!", }); } else { res.send({ message: `Cannot delete Tutorial with id=${id}. Maybe Tutorial was not found!`, }); } }) .catch((err) => { res.status(500).send({ message: "Could not delete Tutorial with id=" + id, }); }); }; // Supprimer tous les tutoriels export const deleteAll = (req, res) => { // Supprimer tous les tutoriels Tutorial.destroy({ where: {}, truncate: false, }) .then((nums) => { res.send({ message: `${nums} Tutorials were deleted successfully!` }); }) .catch((err) => { res.status(500).send({ message: err.message || "Some error occurred while removing all tutorials.", }); }); }; // Trouver tous les tutoriels publiés export const findAllPublished = (req, res) => { // Trouver tous les tutoriels avec published = true Tutorial.findAll({ where: { published: true } }) .then((data) => { res.send(data); }) .catch((err) => { res.status(500).send({ message: err.message || "Some error occurred while retrieving tutorials.", }); }); };
  1. Configurer les routes (app/routes/tutorial.routes.js) :
import * as tutorials from "../controllers/tutorial.controller.js"; import express from "express"; export default (app) => { let router = express.Router(); // Créer un nouveau tutoriel router.post("/", tutorials.create); // Récupérer tous les tutoriels router.get("/", tutorials.findAll); // Récupérer un seul tutoriel avec son id router.get("/:id", tutorials.findOne); // Mettre à jour un tutoriel avec son id router.put("/:id", tutorials.update); // Supprimer un tutoriel avec son id router.delete("/:id", tutorials.deleteOne); // Supprimer tous les tutoriels router.delete("/", tutorials.deleteAll); // Trouver tous les tutoriels publiés router.get("/published", tutorials.findAllPublished); app.use("/api/tutorials", router); };
Demo Icon

Want to try passkeys yourself in a passkeys demo?

Try Passkeys

3.4 Lancer le serveur#

  1. Créer le fichier serveur (server.js) :
import express from "express"; import cors from "cors"; import db from "./app/models/index.js"; import tutorialRoutes from "./app/routes/tutorial.routes.js"; const app = express(); const corsOptions = { origin: "http://localhost:5173", }; app.use(cors(corsOptions)); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Route simple app.get("/", (req, res) => { res.json({ message: "Welcome to the Tutorial Application." }); }); // Routes tutorialRoutes(app); // Synchroniser la base de données db.sequelize.sync().then(() => { console.log("Synced db."); }); const PORT = process.env.PORT || 8080; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}.`); });
  1. Démarrer le serveur :
node server.js

Sortie :

Server is running on port 8080. Synced db.

4. Implémentation du frontend#

Voici l'architecture :

Alternativement, vous pouvez utiliser Redux :

4.1 Structure des fichiers#

La structure finale de vos fichiers ressemblera à ceci :

frontend/ ├─ index.html ├─ package.json ├─ postcss.config.js ├─ tailwind.config.js ├─ vite.config.js ├─ src/ │ ├─ App.jsx │ ├─ main.jsx │ ├─ index.css │ ├─ services/ │ │ └─ tutorial.service.js │ └─ pages/ │ ├─ AddTutorial.jsx │ ├─ Tutorial.jsx │ └─ TutorialsList.jsx
Slack Icon

Become part of our Passkeys Community for updates & support.

Join

4.2 Créer l'application React#

Exécutez les commandes suivantes pour configurer une nouvelle application React avec Vite :

npm create vite@latest frontend -- --template react cd frontend npm i npm i react-router-dom axios npm install -D tailwindcss autoprefixer npx tailwindcss init -p

Enfin, configurez l'option de contenu de Tailwind dans le fichier de configuration tailwind.config.js :

/** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {}, }, plugins: [], };

Ensuite, ouvrez src/index.css (Vite a créé ce fichier pour vous) et ajoutez les directives Tailwind :

@tailwind base; @tailwind components; @tailwind utilities;

4.3 Initialiser la mise en page de l'application#

Le composant App.jsx configure les routes de React Router et met en place une barre de navigation Tailwind de base. Nous naviguerons entre :

  • /tutorials – liste des tutoriels
  • /add – formulaire pour créer un nouveau tutoriel
  • /tutorials/:id – modification d'un seul tutoriel
import { Routes, Route, Link, BrowserRouter } from "react-router-dom"; import TutorialsList from "./pages/TutorialsList"; import AddTutorial from "./pages/AddTutorial"; import Tutorial from "./pages/Tutorial"; function App() { return ( <BrowserRouter> <div> {/* BARRE DE NAVIGATION */} <nav className="bg-blue-600 p-4 text-white"> <div className="flex space-x-4"> <Link to="/tutorials" className="hover:text-gray-300 font-bold"> Tutorials </Link> <Link to="/add" className="hover:text-gray-300"> Add </Link> </div> </nav> {/* ROUTES */} <div className="container mx-auto mt-8 px-4"> <Routes> <Route path="/" element={<TutorialsList />} /> <Route path="/tutorials" element={<TutorialsList />} /> <Route path="/add" element={<AddTutorial />} /> <Route path="/tutorials/:id" element={<Tutorial />} /> </Routes> </div> </div> </BrowserRouter> ); } export default App;

4.4 Créer le service de données#

Ce service gère les requêtes HTTP Axios vers notre backend Node/Express (http://localhost:8080/api). Mettez à jour la baseURL si votre serveur s'exécute sur une adresse ou un port différent.

import axios from "axios"; const http = axios.create({ baseURL: "http://localhost:8080/api", headers: { "Content-Type": "application/json", }, }); const getAll = () => { return http.get("/tutorials"); }; const get = (id) => { return http.get(`/tutorials/${id}`); }; const create = (data) => { return http.post("/tutorials", data); }; const update = (id, data) => { return http.put(`/tutorials/${id}`, data); }; const remove = (id) => { return http.delete(`/tutorials/${id}`); }; const removeAll = () => { return http.delete("/tutorials"); }; const findByTitle = (title) => { return http.get(`/tutorials?title=${title}`); }; export default { getAll, get, create, update, remove, removeAll, findByTitle, };

4.5 Composant d'ajout d'élément#

Un composant pour créer de nouveaux tutoriels sous src/pages/AddTutorial.jsx. Il permet de saisir un titre et une description, puis appelle TutorialService.create().

import React, { useState } from "react"; import TutorialService from "../services/tutorial.service"; function AddTutorial() { const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [submitted, setSubmitted] = useState(false); const saveTutorial = () => { const data = { title, description }; TutorialService.create(data) .then((response) => { console.log(response.data); setSubmitted(true); }) .catch((e) => { console.log(e); }); }; const newTutorial = () => { setTitle(""); setDescription(""); setSubmitted(false); }; return ( <div className="max-w-sm mx-auto p-4 bg-white rounded shadow"> {submitted ? ( <div> <h4 className="font-bold text-green-600 mb-4"> Tutorial submitted successfully! </h4> <button className="bg-blue-500 text-white px-3 py-1 rounded" onClick={newTutorial} > Add Another </button> </div> ) : ( <div> <h4 className="font-bold text-xl mb-2">Add Tutorial</h4> <div className="mb-2"> <label className="block mb-1 font-medium">Title</label> <input type="text" className="border border-gray-300 rounded w-full px-2 py-1" value={title} onChange={(e) => setTitle(e.target.value)} /> </div> <div className="mb-2"> <label className="block mb-1 font-medium">Description</label> <input type="text" className="border border-gray-300 rounded w-full px-2 py-1" value={description} onChange={(e) => setDescription(e.target.value)} /> </div> <button className="bg-green-500 text-white px-3 py-1 rounded mt-2" onClick={saveTutorial} > Submit </button> </div> )} </div> ); } export default AddTutorial;
Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

4.6 Composant de la liste des tutoriels#

Un composant sous src/pages/TutorialsList.jsx qui :

  • Affiche une barre de recherche pour filtrer par titre de tutoriel
  • Liste les tutoriels à gauche
  • Affiche le tutoriel sélectionné à droite
  • Fournit un bouton pour supprimer tous les tutoriels
import { useState, useEffect } from "react"; import TutorialService from "../services/tutorial.service"; import { Link } from "react-router-dom"; function TutorialsList() { const [tutorials, setTutorials] = useState([]); const [currentTutorial, setCurrentTutorial] = useState(null); const [currentIndex, setCurrentIndex] = useState(-1); const [searchTitle, setSearchTitle] = useState(""); useEffect(() => { retrieveTutorials(); }, []); const onChangeSearchTitle = (e) => { setSearchTitle(e.target.value); }; const retrieveTutorials = () => { TutorialService.getAll() .then((response) => { setTutorials(response.data); console.log(response.data); }) .catch((e) => { console.log(e); }); }; const refreshList = () => { retrieveTutorials(); setCurrentTutorial(null); setCurrentIndex(-1); }; const setActiveTutorial = (tutorial, index) => { setCurrentTutorial(tutorial); setCurrentIndex(index); }; const removeAllTutorials = () => { TutorialService.removeAll() .then((response) => { console.log(response.data); refreshList(); }) .catch((e) => { console.log(e); }); }; const findByTitle = () => { TutorialService.findByTitle(searchTitle) .then((response) => { setTutorials(response.data); setCurrentTutorial(null); setCurrentIndex(-1); console.log(response.data); }) .catch((e) => { console.log(e); }); }; return ( <div className="flex flex-col lg:flex-row gap-8"> {/* COLONNE GAUCHE : RECHERCHE + LISTE */} <div className="flex-1"> <div className="flex mb-4"> <input type="text" className="border border-gray-300 rounded-l px-2 py-1 w-full" placeholder="Search by title" value={searchTitle} onChange={onChangeSearchTitle} /> <button className="bg-blue-500 text-white px-4 py-1 rounded-r" onClick={findByTitle} > Search </button> </div> <h4 className="font-bold text-lg mb-2">Tutorials List</h4> <ul className="divide-y divide-gray-200 border border-gray-200 rounded"> {tutorials && tutorials.map((tutorial, index) => ( <li className={ "px-4 py-2 cursor-pointer " + (index === currentIndex ? "bg-blue-100" : "") } onClick={() => setActiveTutorial(tutorial, index)} key={index} > {tutorial.title} </li> ))} </ul> <button className="bg-red-500 text-white px-3 py-1 rounded mt-4" onClick={removeAllTutorials} > Remove All </button> </div> {/* COLONNE DROITE : DÉTAILS */} <div className="flex-1"> {currentTutorial ? ( <div className="p-4 bg-white rounded shadow"> <h4 className="font-bold text-xl mb-2">Tutorial</h4> <div className="mb-2"> <strong>Title: </strong> {currentTutorial.title} </div> <div className="mb-2"> <strong>Description: </strong> {currentTutorial.description} </div> <div className="mb-2"> <strong>Status: </strong> {currentTutorial.published ? "Published" : "Pending"} </div> <Link to={`/tutorials/${currentTutorial.id}`} className="inline-block bg-yellow-400 text-black px-3 py-1 rounded" > Edit </Link> </div> ) : ( <div> <p>Please click on a Tutorial...</p> </div> )} </div> </div> ); } export default TutorialsList;
StateOfPasskeys Icon

Want to find out how many people use passkeys?

View Adoption Data

4.7 Composant Tutoriel#

Un composant fonctionnel sous src/pages/Tutorial.jsx pour visualiser et modifier un seul tutoriel. Il utilise :

  • useParams() pour obtenir :id depuis l'URL
  • useNavigate() pour la redirection
  • TutorialService pour les opérations get, update et delete
import { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import TutorialService from "../services/tutorial.service"; function Tutorial() { const { id } = useParams(); const navigate = useNavigate(); const [currentTutorial, setCurrentTutorial] = useState({ id: null, title: "", description: "", published: false, }); const [message, setMessage] = useState(""); const getTutorial = (id) => { TutorialService.get(id) .then((response) => { setCurrentTutorial(response.data); console.log(response.data); }) .catch((e) => { console.log(e); }); }; useEffect(() => { if (id) getTutorial(id); }, [id]); const handleInputChange = (event) => { const { name, value } = event.target; setCurrentTutorial({ ...currentTutorial, [name]: value }); }; const updatePublished = (status) => { const data = { ...currentTutorial, published: status, }; TutorialService.update(currentTutorial.id, data) .then((response) => { setCurrentTutorial({ ...currentTutorial, published: status }); console.log(response.data); }) .catch((e) => { console.log(e); }); }; const updateTutorial = () => { TutorialService.update(currentTutorial.id, currentTutorial) .then((response) => { console.log(response.data); setMessage("The tutorial was updated successfully!"); }) .catch((e) => { console.log(e); }); }; const deleteTutorial = () => { TutorialService.remove(currentTutorial.id) .then((response) => { console.log(response.data); navigate("/tutorials"); }) .catch((e) => { console.log(e); }); }; return ( <div> {currentTutorial ? ( <div className="max-w-sm mx-auto p-4 bg-white rounded shadow"> <h4 className="font-bold text-xl mb-2">Edit Tutorial</h4> <div className="mb-2"> <label className="block font-medium" htmlFor="title"> Title </label> <input type="text" className="border border-gray-300 rounded w-full px-2 py-1" id="title" name="title" value={currentTutorial.title} onChange={handleInputChange} /> </div> <div className="mb-2"> <label className="block font-medium" htmlFor="description"> Description </label> <input type="text" className="border border-gray-300 rounded w-full px-2 py-1" id="description" name="description" value={currentTutorial.description} onChange={handleInputChange} /> </div> <div className="mb-2"> <strong>Status:</strong>{" "} {currentTutorial.published ? "Published" : "Pending"} </div> <div className="space-x-2 mt-2"> {currentTutorial.published ? ( <button className="bg-blue-500 text-white px-3 py-1 rounded" onClick={() => updatePublished(false)} > Unpublish </button> ) : ( <button className="bg-blue-500 text-white px-3 py-1 rounded" onClick={() => updatePublished(true)} > Publish </button> )} <button className="bg-red-500 text-white px-3 py-1 rounded" onClick={deleteTutorial} > Delete </button> <button className="bg-green-500 text-white px-3 py-1 rounded" onClick={updateTutorial} > Update </button> </div> {message && <p className="text-green-600 mt-2">{message}</p>} </div> ) : ( <div> <p>Loading tutorial...</p> </div> )} </div> ); } export default Tutorial;
Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

4.8 Lancer l'application#

npm run dev

Vous pouvez maintenant accéder à l'application à l'adresse http://localhost:5173. Ouvrez cette URL dans votre navigateur. Vous pouvez maintenant naviguer vers :

  • /tutorials – voir tous les tutoriels
  • /add – ajouter un tutoriel
  • /tutorials/:id – modifier un tutoriel spécifique

(Assurez-vous que votre back-end Node/Express est en cours d'exécution sur http://localhost:8080 ou mettez à jour la baseURL dans tutorial.service.js en conséquence.)

5. Conclusion#

Vous avez construit avec succès une application CRUD full-stack en utilisant React, Node.js, Express et MySQL. Ce projet montre comment mettre en place une API RESTful avec Node.js et Express, gérer les données avec l'ORM Sequelize, et créer un frontend réactif avec React et TailwindCSS.

Bon codage et à bientôt dans le prochain tutoriel !

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start for free

Share this article


LinkedInTwitterFacebook

Enjoyed this read?

🤝 Join our Passkeys Community

Share passkeys implementation tips and get support to free the world from passwords.

🚀 Subscribe to Substack

Get the latest news, strategies, and insights about passkeys sent straight to your inbox.

Related Articles