Node/Express-Server fertigstellen und mit React verbinden
Den Server fertigstellen und anbinden
~15 Min. Lesezeit Zuletzt aktualisiert am 8. August 2026
Der letzte Schritt: Wir geben unserem Server echte Bewertungs-Endpunkte, legen die passende Datenbanktabelle an, und verbinden ReviewsPage damit – Bewertungen überleben ab jetzt jeden Neustart der App.
server/db.js erweitern: die Tabelle automatisch anlegen
Erweitern Sie server/db.js um eine Funktion, die beim Start die Tabelle anlegt, falls sie noch nicht existiert:
import mysql from 'mysql2/promise';
export const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'geheimespasswort',
database: 'produktkatalog',
});
export async function initDatabase() {
await pool.query(`
CREATE TABLE IF NOT EXISTS reviews (
id INT AUTO_INCREMENT PRIMARY KEY,
product_sku VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
rating INT NOT NULL,
comment TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
}CREATE TABLE IF NOT EXISTS ist idempotent (mehrfach sicher ausführbar) – beim ersten Start wird die Tabelle angelegt, bei jedem weiteren Start passiert einfach nichts, statt eines Fehlers. product_sku verknüpft jede Bewertung mit genau einem Produkt aus unserer Magento-/dummyjson-API.
server/server.js erweitern: die Bewertungs-Routen
import express from 'express';
import cors from 'cors';
import { pool, initDatabase } from './db.js';
const app = express();
app.use(cors());
app.use(express.json());
app.get('/api/health', async (req, res) => {
const [rows] = await pool.query('SELECT 1 AS ok');
res.json({ status: 'ok', datenbankVerbunden: rows[0].ok === 1 });
});
app.get('/api/products/:sku/reviews', async (req, res) => {
const [rows] = await pool.query(
'SELECT id, name, rating, comment FROM reviews WHERE product_sku = ? ORDER BY created_at DESC',
[req.params.sku]
);
res.json(rows);
});
app.post('/api/products/:sku/reviews', async (req, res) => {
const { name, rating, comment } = req.body;
if (!name || !comment) {
return res.status(400).json({ error: 'name und comment sind erforderlich.' });
}
await pool.query(
'INSERT INTO reviews (product_sku, name, rating, comment) VALUES (?, ?, ?, ?)',
[req.params.sku, name, rating, comment]
);
res.status(201).json({ status: 'erstellt' });
});
initDatabase().then(() => {
app.listen(4000, () => {
console.log('Server läuft auf http://localhost:4000');
});
});Zwei wichtige Details: Die Fragezeichen (?) in den SQL-Befehlen sind "Platzhalter" – mysql2 ersetzt sie sicher durch die Werte aus dem zweiten Array-Argument. Schreiben Sie NIEMALS Nutzereingaben direkt per String-Verkettung in SQL (z. B. `WHERE sku = '${{sku}}'`) – das öffnet SQL-Injection-Angriffen Tür und Tor. initDatabase().then(...) stellt sicher, dass der Server erst startet, NACHDEM die Tabelle garantiert existiert.
src/api/reviewsApi.js anlegen: die Frontend-Seite
const BASE_URL = 'http://localhost:4000/api';
export async function fetchReviews(sku) {
const response = await fetch(`${BASE_URL}/products/${encodeURIComponent(sku)}/reviews`);
if (!response.ok) {
throw new Error(`Server-Fehler: ${response.status}`);
}
return response.json();
}
export async function submitReview(sku, review) {
const response = await fetch(`${BASE_URL}/products/${encodeURIComponent(sku)}/reviews`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(review),
});
if (!response.ok) {
throw new Error(`Server-Fehler: ${response.status}`);
}
return response.json();
}Genau wie api/magentoApi.js ist auch reviewsApi.js die einzige Datei, die weiß, dass unser eigener Server unter localhost:4000 läuft – der Rest der App ruft nur fetchReviews()/submitReview() auf.
ReviewsPage.jsx aktualisieren: echte Bewertungen laden und speichern
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import ReviewForm from '../components/ReviewForm';
import { fetchReviews, submitReview } from '../api/reviewsApi';
function ReviewsPage() {
const { sku } = useParams();
const [reviews, setReviews] = useState([]);
const [ladend, setLadend] = useState(true);
useEffect(() => {
async function laden() {
try {
const data = await fetchReviews(sku);
setReviews(data);
} catch (error) {
console.error(error);
} finally {
setLadend(false);
}
}
laden();
}, [sku]);
async function handleReviewSubmit(review) {
await submitReview(sku, review);
const aktualisiert = await fetchReviews(sku);
setReviews(aktualisiert);
}
if (ladend) {
return <p>Lade Bewertungen...</p>;
}
return (
<div>
<h3>Bewertungen für {sku}</h3>
<ReviewForm onSubmitReview={handleReviewSubmit} />
{reviews.length === 0 ? (
<p>Noch keine Bewertungen.</p>
) : (
<ul>
{reviews.map((review) => (
<li key={review.id}>
<strong>{review.name}</strong> – {review.rating} Sterne
<p>{review.comment}</p>
</li>
))}
</ul>
)}
</div>
);
}
export default ReviewsPage;handleReviewSubmit speichert zuerst per submitReview() auf dem Server UND lädt danach die komplette Liste neu per fetchReviews() – statt die neue Bewertung nur lokal ans bestehende Array anzuhängen (wie noch in Kapitel 16/18). Das stellt sicher, dass die Anzeige exakt dem entspricht, was wirklich in der Datenbank steht, inklusive der vom Server vergebenen id.
Alles zusammen testen
- MySQL läuft:
docker compose up -d(falls noch nicht aktiv) - Server starten, in einem eigenen Terminal:
cd server && node server.js - React-App starten, in einem ZWEITEN Terminal, im Hauptordner:
npm run dev - Im Browser zu einem Produkt navigieren, "Bewertungen ansehen" anklicken, eine Bewertung abschicken
- Seite komplett neu laden (F5) – die Bewertung ist immer noch da
Herzlichen Glückwunsch! Damit ist unser Produktkatalog komplett: eine echte, mehrseitige React-App mit Produktliste, Suche, Detailseiten, Login-Bereich, Formularen und einer selbst gebauten, dauerhaften Bewertungs-Funktion mit eigenem Server und eigener Datenbank – genau die Projektstruktur, die wir uns in Kapitel 3 vorgenommen hatten.
Fertige Projektstruktur – genau die aus Kapitel 3 angekündigte
produktkatalog-web/
├── index.html
├── package.json
├── vite.config.js
├── docker-compose.yml (MySQL-Container für Bewertungen)
├── server/ (eigener Node/Express-Server)
│ ├── package.json
│ ├── server.js
│ └── db.js
└── src/
├── main.jsx
├── App.jsx (Routing)
├── index.css
├── api/
│ ├── magentoApi.js (Magento-REST-API: Produkte)
│ └── reviewsApi.js (eigener Server: Bewertungen)
├── context/
│ └── AuthContext.jsx (einfacher Login-Zustand)
├── components/
│ ├── ProductCard.jsx
│ └── ProtectedRoute.jsx
├── hooks/
│ └── useDocumentTitle.js (Custom Hook)
└── pages/
├── ProductListPage.jsx
├── ProductDetailPage.jsx
├── ReviewsPage.jsx (verschachtelte Route)
├── LoginPage.jsx
└── AccountPage.jsx (geschützte Route)Tipp: Möchten Sie tiefer einsteigen – Performance-Optimierung, TypeScript, State-Management-Bibliotheken wie Redux Toolkit oder Zustand, und einen Blick unter die Haube von React selbst (Fiber, Virtual DOM)? Genau das behandelt die fortgeschrittene Fortsetzung dieses Tutorials: "React für Profis".