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

React, Node.js, Express und MySQL CRUD-App

Dieses Tutorial zeigt Ihnen, wie Sie eine einfache CRUD-App mit React im Frontend und Express im Backend mit einer MySQL-Datenbank erstellen.

Blog-Post-Author

Lukas

Created: June 17, 2025

Updated: June 24, 2025


In diesem umfassenden Tutorial lernen Sie, wie Sie eine Full-Stack-CRUD-Anwendung (Create, Read, Update, Delete) mit React für das Frontend und Node.js mit Express für das Backend erstellen, die auf einer MySQL-Datenbank basiert. Wir werden Technologien wie React Router, Axios und TailwindCSS verwenden, um die Benutzererfahrung zu verbessern.

Debugger Icon

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

Try for Free

1. Projektübersicht#

Wir entwickeln eine Full-Stack-Anwendung, in der Benutzer Tutorials verwalten können. Jedes Tutorial wird die folgenden Attribute haben:

  • ID: Eindeutiger Bezeichner
  • Titel: Name des Tutorials
  • Beschreibung: Detaillierte Informationen
  • Veröffentlichungsstatus: Gibt an, ob das Tutorial veröffentlicht ist

Benutzer können die folgenden Aktionen ausführen:

  • Erstellen eines neuen Tutorials
  • Abrufen aller Tutorials oder eines bestimmten nach ID
  • Aktualisieren bestehender Tutorials
  • Löschen von Tutorials
  • Suchen von Tutorials nach Titel
Subreddit Icon

Discuss passkeys news and questions in r/passkey.

Join Subreddit

Im Folgenden finden Sie einige Beispiel-Screenshots:

1.1 Ein neues Tutorial hinzufügen#

1.2 Alle Tutorials anzeigen#

Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

1.3 Ein Tutorial bearbeiten#

1.4 Tutorials nach Titel suchen#

Substack Icon

Subscribe to our Passkeys Substack for the latest news.

Subscribe

2. Architektur#

Die Anwendung folgt einer Client-Server-Architektur:

  • Backend: Node.js mit Express verarbeitet RESTful-APIs und interagiert über das Sequelize-ORM mit der MySQL-Datenbank.
  • Frontend: React.js kommuniziert über Axios für HTTP-Anfragen mit dem Backend und verwendet den React Router für die Navigation.

3. Backend-Implementierung#

3.1 Node.js-Anwendung einrichten#

  1. Projektverzeichnis erstellen:

    mkdir react-node-express-mysql-crud cd react-node-express-mysql-crud
  2. Node.js-App initialisieren:

    npm init -y
  3. Abhängigkeiten installieren:

    npm install express sequelize mysql2 cors --save
  4. ESModule-Syntax verwenden Fügen Sie die folgende Zeile zu Ihrer package.json-Datei hinzu:

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

3.2 MySQL & Sequelize konfigurieren#

  1. Konfigurationsdatei erstellen (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. Sequelize initialisieren (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. Tutorial-Modell definieren (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; };

Wenn Sie keine MySQL-Datenbank für die lokale Entwicklung haben, können Sie Docker verwenden, um einen temporären MySQL-Container zu erstellen:

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

3.3 Routen und Controller definieren#

  1. Controller erstellen (app/controllers/tutorial.controller.js):
import db from "../models/index.js"; const Op = db.Sequelize.Op; const Tutorial = db.tutorials; // Erstellen und Speichern eines neuen Tutorials export const create = (req, res) => { // Anfrage validieren if (!req.body.title) { res.status(400).send({ message: "Content can not be empty!", }); return; } // Ein Tutorial erstellen const tutorial = { title: req.body.title, description: req.body.description, published: req.body.published ? req.body.published : false, }; // Tutorial in der Datenbank speichern Tutorial.create(tutorial) .then((data) => { res.send(data); }) .catch((err) => { res.status(500).send({ message: err.message || "Some error occurred while creating the Tutorial.", }); }); }; // Alle Tutorials abrufen export const findAll = (req, res) => { // Filterbedingung über Query-Parameter zulassen 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.", }); }); }; // Einzelnes Tutorial nach ID suchen export const findOne = (req, res) => { const id = req.params.id; // Tutorial nach Primärschlüssel suchen 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, }); }); }; // Ein Tutorial nach ID aktualisieren export const update = (req, res) => { const id = req.params.id; // Das Tutorial mit der angegebenen ID aktualisieren 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, }); }); }; // Ein Tutorial nach ID löschen export const deleteOne = (req, res) => { const id = req.params.id; // Das Tutorial mit der angegebenen ID löschen 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, }); }); }; // Alle Tutorials löschen export const deleteAll = (req, res) => { // Alle Tutorials löschen 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.", }); }); }; // Alle veröffentlichten Tutorials finden export const findAllPublished = (req, res) => { // Alle Tutorials mit published = true finden 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. Routen einrichten (app/routes/tutorial.routes.js):
import * as tutorials from "../controllers/tutorial.controller.js"; import express from "express"; export default (app) => { let router = express.Router(); // Ein neues Tutorial erstellen router.post("/", tutorials.create); // Alle Tutorials abrufen router.get("/", tutorials.findAll); // Einzelnes Tutorial mit ID abrufen router.get("/:id", tutorials.findOne); // Ein Tutorial mit ID aktualisieren router.put("/:id", tutorials.update); // Ein Tutorial mit ID löschen router.delete("/:id", tutorials.deleteOne); // Alle Tutorials löschen router.delete("/", tutorials.deleteAll); // Alle veröffentlichten Tutorials finden 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 Den Server ausführen#

  1. Server-Datei erstellen (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 })); // Einfache Route app.get("/", (req, res) => { res.json({ message: "Welcome to the Tutorial Application." }); }); // Routen tutorialRoutes(app); // Datenbank synchronisieren 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. Den Server starten:
node server.js

Ausgabe:

Server is running on port 8080. Synced db.

4. Frontend-Implementierung#

Dies ist die Architektur:

Alternativ können Sie Redux verwenden:

4.1 Dateistruktur#

Ihre endgültige Dateistruktur wird so aussehen:

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 Die React-App erstellen#

Führen Sie die folgenden Befehle aus, um eine neue React-App mit Vite einzurichten:

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

Richten Sie abschließend die Tailwind-Content-Option in der Konfigurationsdatei tailwind.config.js ein:

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

Öffnen Sie dann src/index.css (Vite hat diese Datei für Sie erstellt) und fügen Sie die Tailwind-Direktiven hinzu:

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

4.3 Das App-Layout initialisieren#

Die App.jsx-Komponente konfiguriert die Routen des React Routers und richtet eine einfache Tailwind-Navigationsleiste ein. Wir werden zwischen folgenden Routen navigieren:

  • /tutorials – Liste der Tutorials
  • /add – Formular zum Erstellen eines neuen Tutorials
  • /tutorials/:id – Bearbeiten eines einzelnen Tutorials
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> {/* NAVIGATIONSLEISTE */} <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> {/* ROUTEN */} <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 Daten-Service erstellen#

Dieser Service verarbeitet Axios-HTTP-Anfragen an unser Node/Express-Backend (http://localhost:8080/api). Aktualisieren Sie die baseURL, wenn Ihr Server unter einer anderen Adresse oder einem anderen Port läuft.

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 Komponente zum Hinzufügen von Elementen#

Eine Komponente zum Erstellen neuer Tutorials unter src/pages/AddTutorial.jsx. Sie ermöglicht die Eingabe von Titel und Beschreibung und ruft dann TutorialService.create() auf.

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 Tutorial-Listen-Komponente#

Eine Komponente unter src/pages/TutorialsList.jsx, die:

  • Eine Suchleiste anzeigt, um nach Tutorial-Titeln zu filtern
  • Tutorials auf der linken Seite auflistet
  • Das ausgewählte Tutorial auf der rechten Seite anzeigt
  • Einen Button zum Entfernen aller Tutorials bereitstellt
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"> {/* LINKE SPALTE: SUCHE + 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> {/* RECHTE SPALTE: DETAILS */} <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 Tutorial-Komponente#

Eine Funktionskomponente unter src/pages/Tutorial.jsx zum Anzeigen und Bearbeiten eines einzelnen Tutorials. Sie verwendet:

  • useParams() um :id aus der URL zu erhalten
  • useNavigate() zur Weiterleitung
  • TutorialService für get-, update- und delete-Operationen
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 Die Anwendung ausführen#

npm run dev

Sie können nun unter http://localhost:5173 auf die Anwendung zugreifen. Öffnen Sie diese URL in Ihrem Browser. Sie können jetzt zu folgenden Seiten navigieren:

  • /tutorials – alle Tutorials ansehen
  • /add – ein Tutorial hinzufügen
  • /tutorials/:id – ein bestimmtes Tutorial bearbeiten

(Stellen Sie sicher, dass Ihr Node/Express-Backend unter http://localhost:8080 läuft oder aktualisieren Sie die baseURL in tutorial.service.js entsprechend.)

5. Fazit#

Sie haben erfolgreich eine Full-Stack-CRUD-Anwendung mit React, Node.js, Express und MySQL erstellt. Dieses Projekt zeigt, wie man eine RESTful-API mit Node.js und Express einrichtet, Daten mit dem Sequelize-ORM verwaltet und ein responsives Frontend mit React und TailwindCSS erstellt.

Viel Spaß beim Programmieren und bis zum nächsten Tutorial!

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