18 KiB
Les 8 — Docenttekst
Van In-Memory naar Supabase
Lesoverzicht
| Gegeven | Details |
|---|---|
| Les | 8 van 18 |
| Onderwerp | Supabase koppelen aan Next.js |
| Duur | 3 uur (09:00 – 12:00) |
| Voorbereiding | Werkend QuickPoll project, Supabase project met polls/options tabellen |
| Benodigdheden | Laptop, Cursor/VS Code, browser, Supabase account |
Leerdoelen
Na deze les kunnen studenten:
- De Supabase JavaScript client installeren en configureren
- Environment variables gebruiken voor API keys
- Data ophalen via Supabase queries (select met relaties, eq, single)
- Het verschil uitleggen tussen sync en async data ophalen
- Het Server Component + Client Component patroon toepassen
- Een formulier bouwen dat data INSERT in Supabase
Lesplanning
09:00–09:10 | Welkom & Terugblik (10 min)
📌 Slide 1, 2, 3
Doel: Studenten op dezelfde pagina brengen over waar we zijn.
Wat te zeggen:
- "Vorige week hebben we een werkend polling app gebouwd met in-memory data."
- "Vandaag koppelen we Supabase: onze database-as-a-service."
- "Na vandaag kunnen jullie niet alleen stemmen, maar ook nieuwe polls aanmaken."
Check:
- Iedereen heeft Supabase account met polls en options tabellen
- Iedereen heeft QuickPoll project lokaal runnen op localhost:3000
- Niemand heeft Auth ingesteld (dat doen we volgende les)
09:10–10:15 | DEEL 1: Live Coding — Supabase koppelen (65 min)
📌 Slide 4, 5, 6
Doel: Live voor hen de hele flow bouwen: installatie → queries → component aanpassingen.
Voorbereiding jij:
- Open Cursor met je QuickPoll project
- Zorg dat Supabase dashboard open staat in je browser
npm install @supabase/supabase-jsal gedraaid (zeker weten!)- Terminal gereed, dev server draait
Stap-voor-stap Live Coding:
1. npm install @supabase/supabase-js
npm install @supabase/supabase-js
Zeg: "Dit geeft ons de client om met Supabase te praten."
2. .env.local (Settings → API)
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
Zeg: "Dit zijn jullie API credentials. Ziet erruit in Supabase Settings → API. De NEXT_PUBLIC_ prefix betekent dat deze in de browser beschikbaar zijn (safe)."
Docent tip: Na npm install en .env wijzigen moet de dev server herstarten! Zeg dit expliciet.
3. lib/supabase.ts
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
Zeg: "Dit is onze Supabase client. We maken hem eenmalig aan en exporteren hem, dan kunnen alle componenten hem gebruiken."
4. types/index.ts (Database matching)
export interface Poll {
id: number;
question: string;
created_at: string;
options: Option[];
}
export interface Option {
id: number;
poll_id: number;
text: string;
votes: number;
created_at: string;
}
Zeg: "Dit matchen onze TypeScript types met de database schema. Poll bevat options als relatie."
5. lib/data.ts (Supabase queries herschrijven)
VOOR je dit toont, laat je het oude in-memory array zien:
// OUD:
const polls = [
{ question: "...", options: ["...", "..."], votes: [0, 0] }
];
export function getPolls() {
return polls;
}
Zeg: "Dit was in-memory. Nu halen we het uit Supabase."
NA - Supabase queries:
import { supabase } from "./supabase";
import { Poll } from "@/types";
export async function getPolls(): Promise<Poll[]> {
const { data, error } = await supabase
.from("polls")
.select("*, options(*)");
if (error) {
console.error("Error fetching polls:", error);
return [];
}
return data || [];
}
export async function getPollById(id: number): Promise<Poll | null> {
const { data, error } = await supabase
.from("polls")
.select("*, options(*)")
.eq("id", id)
.single();
if (error) {
console.error("Error fetching poll:", error);
return null;
}
return data;
}
export async function votePoll(optionId: number): Promise<boolean> {
const { error } = await supabase.rpc("vote_option", { option_id: optionId });
if (error) {
console.error("Error voting:", error);
return false;
}
return true;
}
Docent tips:
.select("*, options(*)")= "Haal polls op, EN daarbij hun relatie options".eq("id", id)= "Where id = ...".single()= "Ik verwacht exact 1 resultaat"await= Dit is nu async! Componenten moetenasynczijn of we gebruiken een API route
6. PAUZE — Slide 6: Server vs Client: Wie doet wat?
BELANGRIJK: Toon deze slide VOOR je componenten aanpast. Dit patroon is cruciaal.
Zeg:
"We hebben nu async functies. Server Components kunnen await direct gebruiken. Client Components niet. Daarom splitsen we:
- Server Components: /page.tsx files (halen data op met await)
- Client Components: VoteForm (useState, onClick event handlers)"
Laat code zien:
// Server Component
export default async function HomePage() {
const polls = await getPolls();
return <>{...}</>
}
// Client Component
'use client'
export function VoteForm() {
const [voted, setVoted] = useState(false);
return <>{...}</>
}
7. app/page.tsx → Server Component
import { getPolls } from "@/lib/data";
import Link from "next/link";
import PollItem from "@/components/PollItem";
export default async function HomePage() {
const polls = await getPolls();
return (
<div className="w-full max-w-2xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-6">Huidige Polls</h1>
<Link href="/create" className="text-blue-600 hover:underline mb-6 block">
+ Nieuwe Poll
</Link>
<div className="space-y-4">
{polls.map((poll) => (
<PollItem key={poll.id} poll={poll} />
))}
</div>
</div>
);
}
Zeg: "Dit is nu async! De await getPolls() werkt hier rechtstreeks. Link naar /create toevoegen."
8. components/PollItem.tsx (Option type, percentage bars)
'use client'
import Link from "next/link";
import { Option } from "@/types";
interface PollItemProps {
poll: {
id: number;
question: string;
options: Option[];
};
}
export default function PollItem({ poll }: PollItemProps) {
const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes, 0);
return (
<div className="border rounded p-4">
<h2 className="text-xl font-semibold mb-4">{poll.question}</h2>
<div className="space-y-2">
{poll.options.map((option) => {
const percentage = totalVotes > 0 ? (option.votes / totalVotes) * 100 : 0;
return (
<Link key={option.id} href={`/poll/${poll.id}`}>
<div className="flex items-center gap-2 cursor-pointer hover:opacity-80">
<div className="flex-1 bg-gray-200 rounded h-8 overflow-hidden">
<div
className="bg-blue-600 h-full transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="text-sm font-medium w-20">
{option.text} ({option.votes})
</span>
</div>
</Link>
);
})}
</div>
</div>
);
}
Zeg: "Nu hebben we Option type beschikbaar. Percentage bars tonen stemmen visueel."
9. components/VoteForm.tsx (Client Component met vote mutation)
'use client'
import { useState } from "react";
import { votePoll } from "@/lib/data";
import { Option } from "@/types";
interface VoteFormProps {
options: Option[];
}
export default function VoteForm({ options }: VoteFormProps) {
const [loading, setLoading] = useState(false);
const [voted, setVoted] = useState(false);
const handleVote = async (optionId: number) => {
setLoading(true);
const success = await votePoll(optionId);
if (success) {
setVoted(true);
}
setLoading(false);
};
if (voted) {
return <p className="text-green-600">Dank je voor je stem!</p>;
}
return (
<div className="space-y-2">
{options.map((option) => (
<button
key={option.id}
onClick={() => handleVote(option.id)}
disabled={loading}
className="w-full bg-blue-600 text-white p-2 rounded hover:bg-blue-700 disabled:opacity-50"
>
{option.text}
</button>
))}
</div>
);
}
Zeg: "Dit is Client Component: 'use client' bovenaan. We kunnen useState gebruiken, onClick handlers. Na stem, feedback tonen."
10. app/poll/[id]/page.tsx (Server + Client combo)
import { getPollById } from "@/lib/data";
import VoteForm from "@/components/VoteForm";
import { notFound } from "next/navigation";
export default async function PollPage({ params }: { params: { id: string } }) {
const poll = await getPollById(parseInt(params.id));
if (!poll) {
notFound();
}
return (
<div className="w-full max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">{poll.question}</h1>
<VoteForm options={poll.options} />
</div>
);
}
Zeg: "Server Component haalt data. Geeft VoteForm (Client Component) de options door. Best of both worlds!"
11. app/api/polls/[id]/route.ts (GET + POST)
import { getPollById, votePoll } from "@/lib/data";
import { NextRequest, NextResponse } from "next/server";
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const poll = await getPollById(parseInt(params.id));
if (!poll) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json(poll);
}
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const { optionId } = await request.json();
const success = await votePoll(optionId);
return NextResponse.json({ success });
}
12. Test alles!
- Homepage laden → alle polls met opties tonen
- Click poll → detail pagina, stem kan worden gegeven
- Stem geven → votes increment in Supabase
- Controleer in Supabase dashboard → votes kolom stijgt
Docent tips bij Live Coding:
- TypeScript errors: "Soms zien we rode squigglies. Dat is TypeScript die zegt 'ik snap dit type niet'. Hover je eroverheen, meestal is het een
!die je moet toevoegen of een import." - RLS blocking: "Nog krijgen we misschien 'RLS policy violation'. Dat fix je volgende les met Auth. Nu gebruiken we publieke SELECT."
- Env restart: Na .env wijzigen ECHT herstarten. Hardnekkig bug!
- Queries testen: Open Supabase dashboard → SQL Editor → test je select statements daar eerst.
10:15–10:30 | PAUZE (15 min)
📌 Slide 7
10:30–11:30 | DEEL 2: Zelf Doen — /create pagina (60 min)
📌 Slide 8
Doel: Studenten bouwen zelf een formulier om nieuwe polls aan te maken.
Stap 1: Theorie op beamer (15 min)
Zeg: "Nu bouwen jullie zelf de /create pagina. Daarmee kunnen gebruikers nieuwe polls aanmaken. Eerst leg ik het uit, dan doen jullie het zelf."
INSERT queries uitleggen:
Laat dit zien:
// 1. Insert poll
const { data: poll } = await supabase
.from("polls")
.insert({ question: "Wat is je favoriete taal?" })
.select()
.single();
// poll is nu { id: 42, question: "Wat is je favoriete taal?", ... }
// 2. Insert options (meerdere tegelijk)
await supabase.from("options").insert([
{ poll_id: 42, text: "JavaScript", votes: 0 },
{ poll_id: 42, text: "Python", votes: 0 },
{ poll_id: 42, text: "Rust", votes: 0 }
]);
Zeg:
- ".insert() = INSERT statement"
- ".select().single() = geef me terug wat je net inserted, als 1 rij"
- "poll.id gebruiken we dan voor de options"
- "Daarna .insert([...]) meerdere opties in één keer"
- "Dan router.push('/') terug naar homepage"
RLS policy toevoegen:
Laat dit SQL blokje zien (ze moeten dit in Supabase doen):
-- INSERT policy voor polls
CREATE POLICY "Allow public insert on polls"
ON polls FOR INSERT
TO anon
WITH CHECK (true);
-- INSERT policy voor options
CREATE POLICY "Allow public insert on options"
ON options FOR INSERT
TO anon
WITH CHECK (true);
Zeg: "Dit zegt tegen Supabase: 'Iedereen mag INSERT-en op polls en options.' Zonder dit krijgen jullie 'RLS policy violation'. Dit is tijdelijk — volgende week beperken we dit met Auth."
Form outline:
1. Text input voor vraag
2. Meerdere text inputs voor opties (minimum 2)
3. "+ Optie toevoegen" knop
4. "Poll aanmaken" submit knop
5. Bij submit: INSERT in polls, dan INSERT in options, dan redirect("/")
Stap 2: Zelf doen (45 min)
Wat studenten moeten doen:
- RLS policy in Supabase dashboard toevoegen (SQL Editor)
- app/create/page.tsx aanmaken met:
'use client'bovenaan- useState voor question en options array
- Input voor question
- Loop over options, input per optie
- "+ Optie toevoegen" knop (addOption)
- "Poll aanmaken" button (handleSubmit)
- handleSubmit logica:
- Insert poll → krijg poll.id terug
- Insert opties met die poll_id
- Error handling
- router.push("/") na succes
- Homepage (page.tsx) updaten:
- Link naar /create bovenaan
Docent loop ronde:
- Min 0-5: Iedereen aan het werk?
- Min 15: Check of iedereen RLS policy heeft ingesteld. Help als iemand vast zit.
- Min 25: Toon code snippet van useState setup als mensen vragen hebben.
- Min 30: Check of eerste iemand INSERT werkend heeft. Toon in Supabase dashboard hoe je ziet dat poll aangemaakt is.
- Min 45: Ruim 5 min voor finalisatie, vragen, troubleshoot.
Veelvoorkomende problemen:
| Probleem | Oplossing |
|---|---|
| "RLS policy violation" | Zeg: RLS policy toegevoegd in dashboard? Zien we in error message "RLS"? |
| "poll is undefined na insert" | .select().single() weg? Dat moet je toevoegen! |
| "Opties werken niet" | poll.id goed doorgegeven aan insert? Controleer in Supabase options tabel. |
| "Form submit refresh de pagina" | e.preventDefault() in handleSubmit? |
| "Redirect werkt niet" | import { useRouter } bovenaan? const router = useRouter() in component? |
| "Opties array gaat fout" | Laat code zien: const newOptions = [...options]; newOptions[index] = value; setOptions(newOptions); |
11:30–11:45 | Vragen & Reflectie (15 min)
Mogelijke vragen + antwoorden:
V: Wat happens na redirect?
A: De homepage laadt opnieuw. app/page.tsx roept getPolls() aan, die hit Supabase en toont je nieuwe poll.
V: Waarom async/await?
A: Supabase is over het network. We wachten tot het antwoord komt. async zegt "dit kan tijd kosten".
V: Kan ik realtime zien als iemand anders stemt? A: Volgende week! Supabase heeft realtime subscriptions. Daar leren we.
V: Wat is /api/ folder?
A: Dat zijn backend endpoints. Volgende week gebruiken we die meer.
V: Waarom 'use client' in create en vote, maar niet in page?
A: Client = interactief (forms, buttons, state). Server = data fetching. Next.js split dit automatisch.
11:45–12:00 | Huiswerk & Afsluiting (15 min)
📌 Slide 9, 10
Huiswerk:
- /create pagina afmaken (als nog niet klaar in klas)
- Validatie toevoegen:
- Vraag mag niet leeg
- Opties moeten uniek zijn
- Minimaal 2 opties
- Error messages tonen
- Delete functionaliteit:
- Delete knop op PollItem
- Verwijder poll + opties uit Supabase
- Extra (voor snelle studenten):
- SQL queries schrijven (direct in Supabase SQL Editor)
- Realtime subscriptions uittesten
- Styling verbeteren
Zeg: "Volgende week: Supabase Auth. Jullie gaan inloggen en registreren bouwen. En bepalen wie welke polls mag aanmaken. Tot dan!"
Slide 10: Afsluiting
- "Tot volgende week!"
- "Volgende les: Supabase Auth — inloggen, registreren, en bepalen wie wat mag"
Veelvoorkomende problemen & Troubleshoot
| Symptoom | Oorzaak | Oplossing |
|---|---|---|
| "Cannot find module @supabase/supabase-js" | npm install niet gedraaid | npm install @supabase/supabase-js |
| Env vars undefined in browser console | NEXT_PUBLIC_ prefix vergeten OF dev server niet restarted | Restart dev server (npm run dev). Check prefix: NEXT_PUBLIC_SUPABASE_URL |
| "RLS policy violation" on SELECT | RLS enabled, geen SELECT policy | Voor nu: disable RLS in Supabase (Security → RLS → toggle OFF). Volgende les met Auth |
| "RLS policy violation" on INSERT | Geen INSERT policy of RLS restrictief | Voeg INSERT policies toe (zie Deel 2 stap 1) |
| getPolls() returns empty array | Query failed maar geen error | Check: .select() syntax correct? options(*) geindent? Controleer in Supabase SQL Editor |
| TypeScript "Cannot find name 'Poll'" | Import weg | import { Poll } from "@/types" bovenaan |
| "notFound() is not defined" | Import weg | import { notFound } from "next/navigation" |
| Percentage bars werken niet | totalVotes = 0 dus percentage = 0 | Check: votes kolom in Supabase ≠ 0? Stem eenmalig via UI |
| Client form not submitting | e.preventDefault() weg OF loading state blocked | Check handleSubmit: eerst e.preventDefault(), geen return-statements die vorig breken |
| Redirect naar / werkt niet na poll maken | router niet geïmporteerd OF router.push() fout | import { useRouter } from "next/navigation" (niet "next/router"!) |
| Supabase queries slow | Network latency / veel data | Normal! Later: replication, caching, realtime |
Tips voor docenten
- Code live typen, niet copy-paste. Laat typos zien, laat debugging zien. Authentiek!
- Veel pauzes voor vragen. Live Coding voelt snel. Check regelmatig: "Allemaal met me mee?"
- Zelf Doen starten met duidelijke steps: (1) RLS policy, (2) page.tsx, (3) form, (4) submit. Niet: "Bouw de pagina."
- Loop ronde, spot problemen vroeg: Min 15-25 zijn goud voor troubleshoot.
- Toon Supabase dashboard often: "Zie je? De data staat echt in de database!"
- Authenticatie is volgende les: Zeg het af te toe: "Dit beperken we volgende week met Auth."
- Celebrate wins: Eerste student met werkende /create? Toon het aan iedereen!