fix: les 6

This commit is contained in:
2026-03-11 14:07:00 +01:00
parent d5066021ab
commit 9ffdecf2c4
117 changed files with 13198 additions and 5194 deletions

View File

@@ -1,412 +1,372 @@
# Les 7: Next.js Fundamentals 2 - API Routes & Data Fetching
# Les 7: Database Principles
---
## Hoofdstuk
**Deel 2: Technical Foundations** (Les 5-9)
**Deel 2: Technical Foundations** (Les 4-8)
## Beschrijving
Leer data fetching in Next.js: Server Components, Client Components, API routes en React Query. Bouw een volledig werkende app met data.
Leer de basisprincipes van relationele databases voordat we Supabase gaan gebruiken. Begrijp tabellen, relaties, keys en normalisatie - essentiële kennis voor elke developer.
---
## Te Behandelen
### Server Components vs Client Components
### Wat is een Relationele Database?
**Server Components (default in Next.js):**
- Renderen op de server
- Geen JavaScript naar de browser
- Kunnen direct data fetchen (async/await)
- Kunnen NIET: useState, useEffect, event handlers
**Een database is:** Een georganiseerde verzameling van data.
**Client Components:**
- Renderen in de browser
- JavaScript naar de browser
- Kunnen interactief zijn
- Markeer met `'use client'` bovenaan
**Relationeel betekent:** Data is opgeslagen in tabellen die aan elkaar gerelateerd zijn.
**Vergelijk het met Excel:**
- Database = Excel workbook
- Tabel = Excel sheet
- Kolom = Excel kolom (field)
- Rij = Excel rij (record)
---
### Wanneer Wat?
### Tabellen, Kolommen en Rijen
```
Server Component → Data tonen, geen interactie
Client Component → Interactie nodig (forms, buttons, state)
```
**Voorbeeld: Users tabel**
| id | name | email | created_at |
|----|------|-------|------------|
| 1 | Tim | tim@email.com | 2024-01-15 |
| 2 | Anna | anna@email.com | 2024-01-16 |
| 3 | Jan | jan@email.com | 2024-01-17 |
**Terminologie:**
- **Tabel:** users
- **Kolommen:** id, name, email, created_at
- **Rijen:** 3 records (Tim, Anna, Jan)
- **Cell:** Eén specifieke waarde (bijv. "tim@email.com")
---
### Data Types
Elke kolom heeft een type:
| Type | Beschrijving | Voorbeeld |
|------|--------------|-----------|
| `text` / `varchar` | Tekst | "Tim", "Hello world" |
| `integer` / `int` | Hele getallen | 1, 42, -5 |
| `decimal` / `numeric` | Decimalen | 19.99, 3.14 |
| `boolean` | True/False | true, false |
| `timestamp` | Datum + tijd | 2024-01-15 14:30:00 |
| `uuid` | Unieke identifier | a1b2c3d4-e5f6-... |
**Kies het juiste type:**
- Prijs? → `decimal` (niet `integer`, want centen)
- Is actief? → `boolean`
- Naam? → `text`
- Aantal? → `integer`
---
### Primary Keys
**Wat:** Een kolom die elke rij UNIEK identificeert.
**Regels:**
- Moet uniek zijn per rij
- Mag nooit NULL zijn
- Verandert nooit
**Voorbeeld:**
```tsx
// Server Component - data ophalen
async function ProductList() {
const products = await fetchProducts() // Direct fetchen!
return <ul>{products.map(p => <li>{p.name}</li>)}</ul>
}
```
users
------
id (PRIMARY KEY) | name | email
1 | Tim | tim@email.com
2 | Anna | anna@email.com
```
// Client Component - interactie
'use client'
function AddToCartButton({ productId }) {
const [added, setAdded] = useState(false)
return <button onClick={() => setAdded(true)}>Add</button>
}
**Waarom niet `email` als primary key?**
- Emails kunnen veranderen
- `id` is stabiel en snel
---
### Foreign Keys
**Wat:** Een kolom die verwijst naar de primary key van een andere tabel.
**Voorbeeld: Posts tabel**
```
posts
------
id | title | user_id (FOREIGN KEY → users.id)
1 | "Mijn blog" | 1
2 | "Hello world" | 1
3 | "Tips" | 2
```
**Wat zegt dit?**
- Post 1 en 2 zijn van user 1 (Tim)
- Post 3 is van user 2 (Anna)
---
### Relatie Types
**One-to-Many (1:N)** - Meest voorkomend!
```
Eén user → meerdere posts
Eén category → meerdere products
```
**One-to-One (1:1)** - Zeldzaam
```
Eén user → één profile
```
**Many-to-Many (N:N)** - Via tussentabel
```
Posts ↔ Tags (een post heeft meerdere tags, een tag heeft meerdere posts)
```
---
### Data Fetching in Server Components
### One-to-Many Voorbeeld
Simpelweg `async/await` gebruiken:
```
users posts
------ ------
id | name id | title | user_id
1 | Tim ←────────── 1 | "Blog 1" | 1
2 | Anna ←────┬───── 2 | "Blog 2" | 1
└───── 3 | "Tips" | 2
```
```tsx
// app/products/page.tsx
interface Product {
id: number
name: string
price: number
}
**Lees:** Tim heeft 2 posts, Anna heeft 1 post.
async function getProducts(): Promise<Product[]> {
const res = await fetch('https://api.example.com/products')
return res.json()
}
---
export default async function ProductsPage() {
const products = await getProducts()
### Many-to-Many met Tussentabel
return (
<div>
<h1>Producten</h1>
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - {product.price}
</li>
))}
</ul>
</div>
)
}
```
posts post_tags tags
------ --------- ------
id | title post_id | tag_id id | name
1 | "React tips" 1 | 1 1 | "react"
2 | "CSS guide" 1 | 2 2 | "frontend"
2 | 2 3 | "css"
2 | 3
```
**Lees:**
- Post 1 heeft tags: react, frontend
- Post 2 heeft tags: frontend, css
---
### Normalisatie Basics
**Probleem: Data duplicatie**
```
orders (SLECHT)
------
id | customer_name | customer_email | product_name | price
1 | Tim | tim@email.com | Laptop | 999
2 | Tim | tim@email.com | Phone | 699
3 | Anna | anna@email.com | Laptop | 999
```
**Problemen:**
- Tim's email staat 2x (als hij verandert: 2 plekken updaten)
- "Laptop" en prijs staan 2x
---
### Genormaliseerde Versie
```
users products orders
------ -------- ------
id | name | email id | name | price id | user_id | product_id
1 | Tim | tim@... 1 | Laptop | 999 1 | 1 | 1
2 | Anna | anna@... 2 | Phone | 699 2 | 1 | 2
3 | 2 | 1
```
**Voordelen:**
- Elk gegeven staat 1x
- Update op 1 plek
- Minder opslagruimte
---
### NULL Values
**NULL = "geen waarde" (niet 0, niet "")**
```
users
------
id | name | phone
1 | Tim | 0612345678
2 | Anna | NULL ← Geen telefoon bekend
```
**Wanneer NULL toestaan?**
- Optionele velden (phone, description)
- Niet bij verplichte velden (name, email)
---
### Defaults
**Automatische waarde als je niks opgeeft:**
```
todos
------
id | title | completed | created_at
| | DEFAULT: false | DEFAULT: now()
```
Bij `INSERT INTO todos (title) VALUES ('Test')`:
```
id | title | completed | created_at
1 | Test | false | 2024-01-15 10:30:00
```
---
### API Routes (Route Handlers)
### Database Schema Tekenen
Bouw je eigen API in Next.js:
**Tools:** draw.io, Excalidraw, pen en papier
**Folder structuur:**
**Conventie:**
```
app/
└── api/
└── products/
── route.ts → /api/products
┌──────────────┐ ┌──────────────┐
│ users │ │ posts │
├──────────────┤ ├──────────────┤
│ id (PK) ───┐ │ id (PK) │
│ name │ │ │ title │
│ email │ └────→│ user_id (FK) │
│ created_at │ │ content │
└──────────────┘ │ created_at │
└──────────────┘
```
**GET request (`app/api/products/route.ts`):**
```typescript
import { NextResponse } from 'next/server'
const products = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Phone', price: 699 },
]
export async function GET() {
return NextResponse.json(products)
}
```
**POST request:**
```typescript
export async function POST(request: Request) {
const body = await request.json()
const newProduct = {
id: Date.now(),
name: body.name,
price: body.price,
}
products.push(newProduct)
return NextResponse.json(newProduct, { status: 201 })
}
```
---
### 'use client' Directive
Wanneer je interactie nodig hebt:
```tsx
'use client' // MOET bovenaan!
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
)
}
```
---
### React Query (TanStack Query)
**Waarom React Query?**
- Automatische caching
- Loading en error states
- Refetching (focus, interval)
- Optimistic updates
**Installatie:**
```bash
npm install @tanstack/react-query
```
**Setup Provider (`app/providers.tsx`):**
```tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
```
**In Layout:**
```tsx
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
```
---
### useQuery - Data Ophalen
```tsx
'use client'
import { useQuery } from '@tanstack/react-query'
interface Product {
id: number
name: string
price: number
}
async function fetchProducts(): Promise<Product[]> {
const res = await fetch('/api/products')
return res.json()
}
export function ProductList() {
const { data, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
})
if (isLoading) return <div>Laden...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data?.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}
```
---
### useMutation - Data Wijzigen
```tsx
'use client'
import { useMutation, useQueryClient } from '@tanstack/react-query'
interface NewProduct {
name: string
price: number
}
async function createProduct(product: NewProduct) {
const res = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(product),
})
return res.json()
}
export function AddProductForm() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createProduct,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['products'] })
},
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
mutation.mutate({ name: 'New Product', price: 99 })
}
return (
<form onSubmit={handleSubmit}>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Toevoegen...' : 'Voeg toe'}
</button>
</form>
)
}
```
---
### Combineren: Server + Client
```tsx
// app/products/page.tsx (Server Component)
import { ProductList } from './product-list'
import { AddProductForm } from './add-product-form'
export default function ProductsPage() {
return (
<div>
<h1>Producten</h1>
<AddProductForm /> {/* Client Component */}
<ProductList /> {/* Client Component */}
</div>
)
}
```
PK = Primary Key
FK = Foreign Key
Pijl = Relatie richting
---
## Tools
- Next.js 14
- React Query (TanStack Query)
- TypeScript
- OpenCode/WebStorm
- Pen en papier / Excalidraw / draw.io
- Supabase Table Editor (vooruitblik)
---
## Lesopdracht (2 uur)
### Bouw CRUD App met API Routes
### Database Design Oefening
**Deel 1: API Routes (40 min)**
**Deel 1: Blog Database Ontwerpen (45 min)**
1. Maak `app/api/products/route.ts`
2. Implementeer GET (alle producten)
3. Implementeer POST (product toevoegen)
4. Test met browser/Postman: `/api/products`
Ontwerp een database voor een blog met:
- Users (kunnen posts schrijven)
- Posts (hebben een auteur)
- Comments (op posts, door users)
**Deel 2: React Query Setup (20 min)**
Voor elke tabel:
1. Teken de tabel met kolommen
2. Bepaal data types
3. Markeer Primary Keys
4. Markeer Foreign Keys
5. Teken de relaties
1. Installeer `@tanstack/react-query`
2. Maak `app/providers.tsx`
3. Wrap app in `QueryClientProvider`
**Deel 2: Normalisatie Oefening (30 min)**
**Deel 3: Data Tonen met useQuery (30 min)**
Gegeven deze "slechte" tabel:
1. Maak `ProductList` Client Component
2. Gebruik `useQuery` om data te fetchen
3. Toon loading state
4. Toon error state
5. Render product lijst
```
library
-------
book_title | author_name | author_email | borrower_name | borrowed_date
"1984" | "Orwell" | orwell@... | "Tim" | 2024-01-15
"1984" | "Orwell" | orwell@... | "Anna" | 2024-01-10
"Dune" | "Herbert" | herbert@... | "Tim" | 2024-01-12
```
**Deel 4: Data Toevoegen met useMutation (30 min)**
Normaliseer naar aparte tabellen:
1. Welke entiteiten zie je?
2. Maak aparte tabellen
3. Voeg relaties toe
1. Maak `AddProductForm` Client Component
2. Gebruik `useMutation` voor POST
3. Invalidate query na success
4. Toon "Adding..." state
**Deel 3: Eindproject Schema (45 min)**
Ontwerp het database schema voor jouw eindproject:
1. Welke entiteiten heb je nodig?
2. Teken elke tabel met kolommen
3. Bepaal relaties
4. Documenteer je keuzes
### Deliverable
- Werkende API routes (GET, POST)
- ProductList met useQuery
- AddProductForm met useMutation
- Loading en error states
- Blog database schema (tekening)
- Genormaliseerde library database
- Eindproject database schema
---
## Huiswerk (2 uur)
### Volledige CRUD Interface
### Verdieping en Voorbereiding
**Deel 1: PUT en DELETE Routes (45 min)**
**Deel 1: Eindproject Schema Uitwerken (1 uur)**
1. Maak `app/api/products/[id]/route.ts`
2. Implementeer PUT (update product)
3. Implementeer DELETE (verwijder product)
4. Test beide endpoints
Werk je database schema volledig uit:
**Deel 2: Update Functionaliteit (45 min)**
1. **Per tabel:**
- Naam
- Alle kolommen met data types
- Primary key
- Foreign keys
- Defaults
- Nullable fields
1. Maak edit form in ProductList
2. Gebruik useMutation voor PUT
3. Inline editing OF modal
4. Invalidate query na success
2. **Documenteer:**
- Waarom deze structuur?
- Welke relaties?
- Eventuele alternatieve overwegingen
**Deel 3: Delete Functionaliteit (30 min)**
**Deel 2: Supabase Account (30 min)**
1. Voeg delete button toe per product
2. Gebruik useMutation voor DELETE
3. Voeg confirmation dialog toe
4. Invalidate query na success
Bereid je voor op volgende les:
1. Maak account op [supabase.com](https://supabase.com)
2. Verken de interface
3. Bekijk de Table Editor
**Bonus:** Optimistic Updates
- Product direct uit UI verwijderen
- Rollback als server faalt
**Deel 3: Reflectie (30 min)**
Beantwoord deze vragen (kort):
1. Wat is het verschil tussen primary en foreign key?
2. Waarom normaliseren we data?
3. Wanneer gebruik je one-to-many vs many-to-many?
4. Welke tabellen heeft jouw eindproject nodig?
### Deliverable
- Complete CRUD API (GET, POST, PUT, DELETE)
- UI voor alle operaties
- Error handling
- Optimistic updates (bonus)
- Volledig uitgewerkt database schema voor eindproject
- Supabase account aangemaakt
- Reflectie vragen beantwoord
---
## Leerdoelen
Na deze les kan de student:
- Uitleggen wanneer Server vs Client Components
- De 'use client' directive correct gebruiken
- Data fetchen in Server Components met async/await
- API routes maken met Route Handlers
- GET en POST requests implementeren
- React Query installeren en configureren
- useQuery gebruiken voor data fetching
- useMutation gebruiken voor data mutations
- Loading en error states afhandelen
- Query invalidation toepassen
- Uitleggen wat een relationele database is
- Tabellen, kolommen en rijen beschrijven
- De juiste data types kiezen
- Primary keys en hun doel uitleggen
- Foreign keys en relaties begrijpen
- One-to-many en many-to-many relaties herkennen
- Het probleem van data duplicatie identificeren
- Een database normaliseren
- NULL values en defaults begrijpen
- Een database schema ontwerpen en tekenen