Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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:

server/db.js
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

server/server.js
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

src/api/reviewsApi.js
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

src/pages/ReviewsPage.jsx
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

  1. MySQL läuft: docker compose up -d (falls noch nicht aktiv)
  2. Server starten, in einem eigenen Terminal: cd server && node server.js
  3. React-App starten, in einem ZWEITEN Terminal, im Hauptordner: npm run dev
  4. Im Browser zu einem Produkt navigieren, "Bewertungen ansehen" anklicken, eine Bewertung abschicken
  5. 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".