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

Finishing the Node/Express Server and Connecting It to React

Finishing and Connecting the Server

~15 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

The final step: we give our server real review endpoints, create the matching database table, and connect ReviewsPage to it – reviews now survive every restart of the app.

Extending server/db.js: creating the table automatically

Extend server/db.js with a function that creates the table on startup if it doesn't exist yet:

server/db.js
import mysql from 'mysql2/promise';

export const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  password: 'secretpassword',
  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 is idempotent (safe to run multiple times) – on the first start the table gets created, on every subsequent start nothing happens instead of an error. product_sku links each review to exactly one product from our Magento/dummyjson API.

Extending server/server.js: the review routes

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', databaseConnected: 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 and comment are required.' });
  }

  await pool.query(
    'INSERT INTO reviews (product_sku, name, rating, comment) VALUES (?, ?, ?, ?)',
    [req.params.sku, name, rating, comment]
  );

  res.status(201).json({ status: 'created' });
});

initDatabase().then(() => {
  app.listen(4000, () => {
    console.log('Server running at http://localhost:4000');
  });
});

Two important details: the question marks (?) in the SQL statements are "placeholders" – mysql2 safely substitutes them with the values from the second array argument. NEVER write user input directly into SQL via string concatenation (e.g. `WHERE sku = '${{sku}}'`) – that opens the door wide to SQL injection attacks. initDatabase().then(...) ensures the server only starts AFTER the table is guaranteed to exist.

Creating src/api/reviewsApi.js: the frontend side

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 error: ${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 error: ${response.status}`);
  }

  return response.json();
}

Just like api/magentoApi.js, reviewsApi.js is the only file that knows our own server runs at localhost:4000 – the rest of the app only calls fetchReviews()/submitReview().

Updating ReviewsPage.jsx: loading and saving real reviews

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 [loading, setLoading] = useState(true);

  useEffect(() => {
    async function load() {
      try {
        const data = await fetchReviews(sku);
        setReviews(data);
      } catch (error) {
        console.error(error);
      } finally {
        setLoading(false);
      }
    }
    load();
  }, [sku]);

  async function handleReviewSubmit(review) {
    await submitReview(sku, review);
    const updated = await fetchReviews(sku);
    setReviews(updated);
  }

  if (loading) {
    return <p>Loading reviews...</p>;
  }

  return (
    <div>
      <h3>Reviews for {sku}</h3>
      <ReviewForm onSubmitReview={handleReviewSubmit} />

      {reviews.length === 0 ? (
        <p>No reviews yet.</p>
      ) : (
        <ul>
          {reviews.map((review) => (
            <li key={review.id}>
              <strong>{review.name}</strong> – {review.rating} stars
              <p>{review.comment}</p>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default ReviewsPage;

handleReviewSubmit first saves via submitReview() on the server AND then reloads the entire list via fetchReviews() – instead of just appending the new review locally to the existing array (as it still did in chapters 16/18). This ensures the display exactly matches what's really in the database, including the id assigned by the server.

Testing it all together

  1. MySQL running: docker compose up -d (if not already active)
  2. Start the server, in its own terminal: cd server && node server.js
  3. Start the React app, in a SECOND terminal, in the root folder: npm run dev
  4. In the browser, navigate to a product, click "See reviews", submit a review
  5. Fully reload the page (F5) – the review is still there

Congratulations! Our product catalog is now complete: a real, multi-page React app with a product list, search, detail pages, a login area, forms, and a self-built, persistent review feature with its own server and database – exactly the project structure we set out to build in chapter 3.

Finished project structure – exactly what we announced in chapter 3

produktkatalog-web/
├── index.html
├── package.json
├── vite.config.js
├── docker-compose.yml            (MySQL container for reviews)
├── server/                       (our own Node/Express server)
│   ├── package.json
│   ├── server.js
│   └── db.js
└── src/
    ├── main.jsx
    ├── App.jsx                   (routing)
    ├── index.css
    ├── api/
    │   ├── magentoApi.js         (Magento REST API: products)
    │   └── reviewsApi.js         (our own server: reviews)
    ├── context/
    │   └── AuthContext.jsx       (simple login state)
    ├── components/
    │   ├── ProductCard.jsx
    │   └── ProtectedRoute.jsx
    ├── hooks/
    │   └── useDocumentTitle.js   (custom hook)
    └── pages/
        ├── ProductListPage.jsx
        ├── ProductDetailPage.jsx
        ├── ReviewsPage.jsx       (nested route)
        ├── LoginPage.jsx
        └── AccountPage.jsx       (protected route)

Tipp: Want to go deeper – performance optimization, TypeScript, state management libraries like Redux Toolkit or Zustand, and a look under React's own hood (Fiber, Virtual DOM)? That's exactly what the advanced continuation of this tutorial covers: "React für Profis".