Wat is het Probleem?

Je hebt werkende backend (Express API) op http://localhost:4000. Je wilt frontend (React app) maken op http://localhost:5173.

Als frontend data ophaalt van backend, krijg je deze error:

CORS Error:

Access to fetch at 'http://localhost:4000/api/workouts' from origin 
'http://localhost:5173' has been blocked by CORS policy

Wat is CORS?

CORS = Cross-Origin Resource Sharing

Probleem: Backend (poort 4000) en frontend (poort 5173) draaien op verschillende poorten. Dat zijn technisch verschillende "origins".

Oplossing: Backend moet expliciet toestemming geven aan frontend. Dat doe je met cors package.

Stap 1: CORS in Backend

Je hebt CORS waarschijnlijk al geïnstalleerd. Zo niet:

npm install cors

Open server.js en voeg CORS toe:

import express from 'express';
import cors from 'cors';
import workoutRoutes from './routes/workoutRoutes.js';

const app = express();

// CORS toestaan voor frontend
app.use(cors({
    origin: 'http://localhost:5173'
}));

// Middleware voor JSON
app.use(express.json());

// Routes
app.use('/api/workouts', workoutRoutes);
Wat gebeurt hier?
  • app.use(cors(...)) - Geeft toestemming aan frontend
  • origin: 'http://localhost:5173' - Staat alleen deze URL toe
  • Dit moet VOOR app.use(express.json())

Belangrijk!

CORS moet bovenaan in server.js, voor alle andere middleware. Herstart backend na deze wijziging.

Stap 2: React Frontend met Vite

We gebruiken Vite omdat het:

  • Super snel is
  • Makkelijk op te zetten
  • Hot reload heeft (wijzigingen direct zien)

Maak nieuw React project

Open NIEUWE terminal (laat backend draaien!). Ga naar hoofdfolder waar backend/ in staat.

# Als je in backend/ zit, ga omhoog
cd ..

# Check waar je bent - je zou 'backend' moeten zien
ls

# Maak Vite project
npm create vite@latest

Terminal blijft hangen?

Als terminal vraagt om packages te installeren, druk y en Enter.

Vite stelt vragen. Vul in:

Invullen:

  • Project name: frontend
  • Framework: React (met pijltjes)
  • Variant: JavaScript
# Ga in frontend folder
cd frontend

# Installeer packages
npm install

# Start frontend
npm run dev

Het werkt!

Je ziet: Local: http://localhost:5173/
Open deze URL in browser. Je ziet werkende React app.

Project Structuur

Je hebt nu:

mijn-project/
├── backend/
│   ├── controllers/
│   ├── models/
│   ├── routes/
│   ├── server.js
│   └── package.json
│
└── frontend/
    ├── src/
    │   ├── App.jsx
    │   └── main.jsx
    ├── index.html
    └── package.json

Twee terminals

Terminal 1: Backend op poort 4000
Terminal 2: Frontend op poort 5173

Stap 3: Data Ophalen in React

Open frontend/src/App.jsx en vervang volledige inhoud:

import { useEffect, useState } from 'react';

function App() {
  const [workouts, setWorkouts] = useState([]);

  const fetchWorkouts = async () => {
    try {
      const response = await fetch('http://localhost:4000/api/workouts');
      const data = await response.json();
      setWorkouts(data);
    } catch (error) {
      console.error('Error:', error);
    }
  };

  useEffect(() => {
    fetchWorkouts();
  }, []);

  return (
    <div className="App">
      <h1>Workouts</h1>
      {workouts.length === 0 ? (
        <p>Geen workouts gevonden</p>
      ) : (
        workouts.map(workout => (
          <div key={workout._id}>
            <h3>{workout.title}</h3>
            <p>Reps: {workout.reps}</p>
            <p>Load: {workout.load} kg</p>
          </div>
        ))
      )}
    </div>
  );
}

export default App;
Wat gebeurt hier?
  • useState([]) - Bewaar workouts in state
  • useEffect - Draait automatisch bij laden
  • fetchWorkouts - Async functie voor data ophalen
  • await fetch('...') - Wacht op response van backend
  • await response.json() - Wacht tot JSON geparsed is
  • try/catch - Vangt errors op
  • .map() - Loopt door workouts en toont ze

Testen

Open http://localhost:5173 in browser.

Het werkt als:

  • Je ziet "Workouts" als titel
  • Je ziet workouts uit database
  • Geen CORS errors in console (F12 → Console)

Veelvoorkomende Problemen:

1. CORS error nog steeds

  • Check CORS in server.js
  • Herstart backend
  • Check URL: origin: 'http://localhost:5173'

2. "Geen workouts gevonden"

  • Database is leeg
  • Maak workouts met Postman (POST request)

3. Backend draait niet

  • Check terminal 1 of backend draait
  • Start opnieuw: npm run dev

Bonus: POST Request vanuit React

Je kunt ook nieuwe workouts aanmaken vanuit React. Voorbeeld:

import { useState } from 'react';

function WorkoutForm() {
  const [title, setTitle] = useState('');
  const [reps, setReps] = useState('');
  const [load, setLoad] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();

    const workout = { 
      title, 
      reps: Number(reps), 
      load: Number(load) 
    };

    const response = await fetch('http://localhost:4000/api/workouts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(workout)
    });

    const data = await response.json();

    if (response.ok) {
      console.log('Workout aangemaakt!', data);
      // Reset form
      setTitle('');
      setReps('');
      setLoad('');
    } else {
      console.error('Error:', data.error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Titel"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <input
        type="number"
        placeholder="Reps"
        value={reps}
        onChange={(e) => setReps(e.target.value)}
      />
      <input
        type="number"
        placeholder="Load (kg)"
        value={load}
        onChange={(e) => setLoad(e.target.value)}
      />
      <button type="submit">Toevoegen</button>
    </form>
  );
}
Wat gebeurt hier?
  • useState - Voor elk input veld
  • handleSubmit - Stuurt data naar backend bij submit
  • method: 'POST' - Nieuwe data aanmaken
  • headers - Vertelt backend dat we JSON sturen
  • body: JSON.stringify(workout) - Zet object om naar JSON
  • Reset form als gelukt

UPDATE Request vanuit React

Een workout aanpassen. Je hebt het ID nodig van de workout die je wilt aanpassen.

import { useState } from 'react';

function UpdateWorkout({ workoutId, currentTitle, currentReps, currentLoad }) {
  const [title, setTitle] = useState(currentTitle);
  const [reps, setReps] = useState(currentReps);
  const [load, setLoad] = useState(currentLoad);

  const handleUpdate = async (e) => {
    e.preventDefault();

    const updatedWorkout = { 
      title, 
      reps: Number(reps), 
      load: Number(load) 
    };

    try {
      const response = await fetch(`http://localhost:4000/api/workouts/${workoutId}`, {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(updatedWorkout)
      });

      const data = await response.json();

      if (response.ok) {
        console.log('Workout aangepast!', data);
      } else {
        console.error('Error:', data.error);
      }
    } catch (error) {
      console.error('Fetch error:', error);
    }
  };

  return (
    <form onSubmit={handleUpdate}>
      <input
        type="text"
        placeholder="Titel"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <input
        type="number"
        placeholder="Reps"
        value={reps}
        onChange={(e) => setReps(e.target.value)}
      />
      <input
        type="number"
        placeholder="Load (kg)"
        value={load}
        onChange={(e) => setLoad(e.target.value)}
      />
      <button type="submit">Aanpassen</button>
    </form>
  );
}
Wat gebeurt hier?
  • workoutId - Props met ID van workout die aangepast wordt
  • useState(currentTitle) - Pre-fill form met huidige waarden
  • method: 'PATCH' - UPDATE operatie
  • `/api/workouts/${workoutId}` - URL met specifiek ID
  • try/catch - Vangt errors op

Gebruik in App.jsx:

// In je workout lijst
<UpdateWorkout 
  workoutId={workout._id}
  currentTitle={workout.title}
  currentReps={workout.reps}
  currentLoad={workout.load}
/>

DELETE Request vanuit React

Een workout verwijderen. Simpeler dan UPDATE omdat je geen form data hoeft mee te sturen.

function DeleteWorkout({ workoutId, workoutTitle }) {
  
  const handleDelete = async () => {
    // Bevestiging vragen
    if (!confirm(`Weet je zeker dat je "${workoutTitle}" wilt verwijderen?`)) {
      return;
    }

    try {
      const response = await fetch(`http://localhost:4000/api/workouts/${workoutId}`, {
        method: 'DELETE'
      });

      const data = await response.json();

      if (response.ok) {
        console.log('Workout verwijderd!', data);
        // Verwijder uit UI of refresh lijst
      } else {
        console.error('Error:', data.error);
      }
    } catch (error) {
      console.error('Fetch error:', error);
    }
  };

  return (
    <button onClick={handleDelete}>
      Verwijderen
    </button>
  );
}
Wat gebeurt hier?
  • confirm() - Browser popup voor bevestiging
  • method: 'DELETE' - DELETE operatie
  • Geen body nodig - DELETE heeft alleen ID in URL
  • try/catch - Vangt errors op

Belangrijk!

Vergeet niet de workout uit je state te verwijderen na succesvolle DELETE, anders blijft hij in de UI staan terwijl hij uit de database is!

Volledig Voorbeeld: App met CRUD

Hier is een compleet voorbeeld met alle CRUD operaties in één component:

import { useEffect, useState } from 'react';

function App() {
  const [workouts, setWorkouts] = useState([]);
  const [title, setTitle] = useState('');
  const [reps, setReps] = useState('');
  const [load, setLoad] = useState('');

  // READ - Haal alle workouts op
  const fetchWorkouts = async () => {
    try {
      const response = await fetch('http://localhost:4000/api/workouts');
      const data = await response.json();
      setWorkouts(data);
    } catch (error) {
      console.error('Error:', error);
    }
  };

  useEffect(() => {
    fetchWorkouts();
  }, []);

  // CREATE - Nieuwe workout toevoegen
  const handleSubmit = async (e) => {
    e.preventDefault();

    const workout = { 
      title, 
      reps: Number(reps), 
      load: Number(load) 
    };

    try {
      const response = await fetch('http://localhost:4000/api/workouts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(workout)
      });

      const data = await response.json();

      if (response.ok) {
        setWorkouts([data, ...workouts]); // Voeg toe aan lijst
        setTitle('');
        setReps('');
        setLoad('');
      }
    } catch (error) {
      console.error('Error:', error);
    }
  };

  // DELETE - Workout verwijderen
  const handleDelete = async (id) => {
    if (!confirm('Weet je het zeker?')) return;

    try {
      const response = await fetch(`http://localhost:4000/api/workouts/${id}`, {
        method: 'DELETE'
      });

      if (response.ok) {
        setWorkouts(workouts.filter(w => w._id !== id)); // Verwijder uit lijst
      }
    } catch (error) {
      console.error('Error:', error);
    }
  };

  return (
    <div className="App">
      <h1>Workouts</h1>

      {/* CREATE Form */}
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          placeholder="Titel"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
        <input
          type="number"
          placeholder="Reps"
          value={reps}
          onChange={(e) => setReps(e.target.value)}
        />
        <input
          type="number"
          placeholder="Load (kg)"
          value={load}
          onChange={(e) => setLoad(e.target.value)}
        />
        <button type="submit">Toevoegen</button>
      </form>

      {/* READ - Toon workouts */}
      {workouts.length === 0 ? (
        <p>Geen workouts gevonden</p>
      ) : (
        workouts.map(workout => (
          <div key={workout._id}>
            <h3>{workout.title}</h3>
            <p>Reps: {workout.reps}</p>
            <p>Load: {workout.load} kg</p>
            <button onClick={() => handleDelete(workout._id)}>
              Verwijderen
            </button>
          </div>
        ))
      )}
    </div>
  );
}

export default App;
Wat doet deze app?
  • READ: Haalt workouts op bij laden (useEffect)
  • CREATE: Voegt nieuwe workout toe via form
  • DELETE: Verwijdert workout met button
  • State management: Na CREATE/DELETE wordt state geupdate
  • setWorkouts([data, ...workouts]) - Voeg nieuwe workout bovenaan toe
  • filter(w => w._id !== id) - Verwijder workout uit array

Dit is een werkende CRUD app!

Je kunt nu:

  • ✅ Workouts bekijken (READ)
  • ✅ Workouts toevoegen (CREATE)
  • ✅ Workouts verwijderen (DELETE)
  • ✅ Workouts wijzigen (UPDATE)

Samenvatting

Je hebt nu volledig werkende MERN app:

Backend (Express):

  • MongoDB database verbinding
  • Mongoose model en schema
  • API routes voor CRUD operaties
  • CORS configuratie

Frontend (React):

  • Vite development server
  • Data ophalen met fetch
  • Data tonen met map()
  • Formulier voor nieuwe data

Volgende Stappen

Nu CORS en frontend werken, kun je verder met:

  • Authentication toevoegen
  • Styling met CSS