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:
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
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
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
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
- MySQL running:
docker compose up -d(if not already active) - Start the server, in its own terminal:
cd server && node server.js - Start the React app, in a SECOND terminal, in the root folder:
npm run dev - In the browser, navigate to a product, click "See reviews", submit a review
- 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".